diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index c148518..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,33 +0,0 @@ -version: 2.1 - -orbs: - localstack: localstack/platform@2.0.0 - -jobs: - localstack-test: - executor: localstack/default - environment: - CI_PROJECT: ci-demo-1 - DNS_ADDRESS: "0" - steps: - - checkout - - run: docker pull localstack/localstack - - localstack/startup - - run: - name: Deploy app and run test request - command: | - docker logs localstack-main - awslocal s3 mb s3://test123 - awslocal s3 ls - make install - make deploy - make web & - make send-request - code=$? - docker logs localstack-main - exit $code - -workflows: - localstack-test: - jobs: - - localstack-test diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 0000000..f727260 --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,119 @@ +name: Integration Test + +on: + push: + paths-ignore: + - 'README.md' + branches: + - master + pull_request: + branches: + - master + schedule: + # "At 00:00 on Sunday." + - cron: "0 0 * * 0" + workflow_dispatch: + +jobs: + test: + name: Run Integration Tests + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + working-directory: demo/lambdas/ruby + + - name: Setup LocalStack + uses: localstack/setup-localstack@main + with: + image-tag: 'latest' + use-pro: 'true' + configuration: LS_LOG=trace + install-awslocal: 'true' + env: + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + + - name: Install CDK dependencies + run: cd cdk && npm ci + + - name: Bundle Ruby Lambda gems + working-directory: demo/lambdas/ruby + run: | + bundle config set --local path vendor/bundle + bundle install + + - name: Install Node.js Lambda dependencies + run: cd demo/lambdas/nodejs && npm install --omit=dev + + - name: Bootstrap CDK + working-directory: cdk + env: + AWS_ACCESS_KEY_ID: test + AWS_SECRET_ACCESS_KEY: test + AWS_DEFAULT_REGION: us-east-1 + run: npx cdklocal bootstrap + + - name: Deploy CDK stack + working-directory: cdk + env: + AWS_ACCESS_KEY_ID: test + AWS_SECRET_ACCESS_KEY: test + AWS_DEFAULT_REGION: us-east-1 + run: npx cdklocal deploy LocalstackDemoStack --require-approval never + + - name: Upload frontend to S3 + env: + AWS_ACCESS_KEY_ID: test + AWS_SECRET_ACCESS_KEY: test + AWS_DEFAULT_REGION: us-east-1 + AWS_ENDPOINT_URL: http://localhost.localstack.cloud:4566 + run: awslocal s3 sync demo/web/ s3://archive-bucket/ + + - name: Run end-to-end test + env: + AWS_ACCESS_KEY_ID: test + AWS_SECRET_ACCESS_KEY: test + AWS_DEFAULT_REGION: us-east-1 + AWS_ENDPOINT_URL: http://localhost.localstack.cloud:4566 + run: | + echo "Discovering REST API ID..." + apiId=$(awslocal apigateway get-rest-apis --output json \ + | jq -r '.items[] | select(.name=="localstack-demo") | .id') + echo "API ID: $apiId" + + echo "Sending new request..." + reqID=$(curl -sf -d '{}' \ + "http://localhost:4566/_aws/execute-api/$apiId/local/requests" \ + | jq -r .requestID) + echo "Request ID: $reqID" + + echo "Polling for QUEUED -> PROCESSING -> FINISHED..." + for i in $(seq 1 25); do + cur=$(curl -sf \ + "http://localhost:4566/_aws/execute-api/$apiId/local/requests" \ + | jq -r "[.result[] | select(.requestID==\"$reqID\") | .status] | sort | last") + echo " attempt $i: $cur" + [ "$cur" = "FINISHED" ] && break + sleep 3 + done + + echo "Verifying S3 result file..." + awslocal s3 cp "s3://archive-bucket/$reqID/result.txt" - | grep -q "Archive result" \ + && echo "✅ End-to-end test passed!" \ + || (echo "❌ S3 result not found" && exit 1) + + - name: LocalStack logs (on failure) + if: failure() + run: localstack logs diff --git a/.gitignore b/.gitignore index 1521c0f..e7ee024 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ *.pyc -.serverless/ node_modules .idea/ +cdk.out +.DS_Store +vendor/ diff --git a/Makefile b/Makefile index dd70535..f2fedd5 100644 --- a/Makefile +++ b/Makefile @@ -1,31 +1,103 @@ -export AWS_ACCESS_KEY_ID ?= test +SHELL := /bin/bash + +-include .env-gdc-local + +CDIR = cd cdk + +export AWS_ACCESS_KEY_ID ?= test export AWS_SECRET_ACCESS_KEY ?= test -export AWS_DEFAULT_REGION = us-east-1 - -usage: ## Show this help - @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' - -install: ## Install dependencies - npm install - which serverless || npm install -g serverless - which localstack || pip install localstack - -deploy: ## Deploy the app - @make install; \ - echo "Deploying Serverless app to local environment"; \ - SLS_DEBUG=1 serverless deploy --stage local - -send-request: ## Send a test request to the deployed application - @which jq || (echo "jq was not found. Please install it (https://jqlang.github.io/jq/download/) and try again." && exit 1) - @echo Looking up API ID from deployed API Gateway REST APIs ...; \ - apiId=$$(awslocal apigateway get-rest-apis --output json | jq -r '.items[] | select(.name="local-localstack-demo") | .id'); \ - echo Sending request to API Gateway REST APIs ID "$$apiId" ...; \ - requestID=$$(curl -s -d '{}' http://$$apiId.execute-api.localhost.localstack.cloud:4566/local/requests | jq -r .requestID); \ - echo "Received request ID '$$requestID'"; \ - for i in 1 2 3 4 5 6 7 8 9 10; do echo "Polling for processing result to appear in s3://archive-bucket/..."; awslocal s3 ls s3://archive-bucket/ | grep $$requestID && exit; sleep 3; done - -lint: ## Run code linter - @npm run lint - @flake8 demo - -.PHONY: usage install deploy send-request lint +export AWS_DEFAULT_REGION ?= us-east-1 +export AWS_ENDPOINT_URL ?= http://localhost.localstack.cloud:4566 + +usage: ## Show this help in table format + @echo "| Target | Description |" + @echo "|------------------------|-------------------------------------------------------------------|" + @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/:.*##\s*/##/g' | awk -F'##' '{ printf "| %-22s | %-65s |\n", $$1, $$2 }' + +check: ## Check if all required prerequisites are installed + @command -v docker > /dev/null 2>&1 || { echo "Docker is not installed. Please install Docker and try again."; exit 1; } + @command -v node > /dev/null 2>&1 || { echo "Node.js is not installed. Please install Node.js and try again."; exit 1; } + @command -v aws > /dev/null 2>&1 || { echo "AWS CLI is not installed. Please install AWS CLI and try again."; exit 1; } + @command -v awslocal > /dev/null 2>&1 || { echo "awslocal is not installed. Run: pip install awscli-local"; exit 1; } + @command -v localstack > /dev/null 2>&1 || { echo "LocalStack CLI is not installed. Run: pip install localstack"; exit 1; } + @command -v jq > /dev/null 2>&1 || { echo "jq is not installed. See https://jqlang.github.io/jq/download/"; exit 1; } + @test -n "$(LOCALSTACK_AUTH_TOKEN)" || { echo "LOCALSTACK_AUTH_TOKEN is not set. Find your token at https://app.localstack.cloud/workspace/auth-token"; exit 1; } + @echo "All required prerequisites are available." + +install: ## Install CDK and Lambda dependencies + @$(CDIR); if [ ! -d "node_modules" ]; then \ + echo "Installing CDK dependencies..."; \ + npm install; \ + else \ + echo "CDK dependencies already installed."; \ + fi + +bundle-ruby: ## Bundle Ruby 3.3 Lambda gems (requires Docker) + docker run --rm \ + -v "$(shell pwd)/demo/lambdas/ruby:/var/task" \ + -w /var/task \ + ruby:3.3 \ + sh -c "bundle config set --local path vendor/bundle && bundle install" + +bundle-node: ## Install Node.js Lambda production dependencies + cd demo/lambdas/nodejs && npm install --omit=dev + +build: ## Build the CDK TypeScript app + $(CDIR) && npm run build + +bootstrap: ## Bootstrap CDK for LocalStack + $(CDIR) && npm run build && npx cdklocal bootstrap + +deploy: ## Build, deploy CDK stack to LocalStack, and upload frontend + $(MAKE) bundle-ruby bundle-node + $(MAKE) bootstrap + $(CDIR) && npx cdklocal deploy LocalstackDemoStack --require-approval never + @echo "Uploading frontend to s3://archive-bucket/ ..." + awslocal s3 sync demo/web/ s3://archive-bucket/ + @echo "" + @echo "Done! Open http://localhost:4566/archive-bucket/index.html in your browser." + +destroy: ## Destroy the deployed CDK stack on LocalStack + $(CDIR) && npx cdklocal destroy LocalstackDemoStack + +send-request: ## Send a test request and poll S3 for the result + @command -v jq > /dev/null 2>&1 || { echo "jq is not installed. See https://jqlang.github.io/jq/download/"; exit 1; } + @echo "Looking up REST API ID..." + @apiId=$$(awslocal apigateway get-rest-apis --output json | jq -r '.items[] | select(.name=="localstack-demo") | .id'); \ + echo "Sending request to API ID '$$apiId' ..."; \ + reqID=$$(curl -s -d '{}' "http://localhost:4566/_aws/execute-api/$$apiId/local/requests" | jq -r .requestID); \ + echo "Received request ID '$$reqID'"; \ + for i in 1 2 3 4 5 6 7 8 9 10; do \ + echo "Polling s3://archive-bucket/ for result ..."; \ + awslocal s3 ls s3://archive-bucket/ | grep "$$reqID" && exit 0; \ + sleep 3; \ + done; \ + echo "Timed out waiting for result." + +start: ## Start LocalStack in detached mode + @echo "Starting LocalStack..." + @test -n "$(LOCALSTACK_AUTH_TOKEN)" || { echo "LOCALSTACK_AUTH_TOKEN is not set. Find your token at https://app.localstack.cloud/workspace/auth-token"; exit 1; } + @LOCALSTACK_AUTH_TOKEN=$(LOCALSTACK_AUTH_TOKEN) localstack start -d + @echo "LocalStack started successfully." + +stop: ## Stop LocalStack + @echo "Stopping LocalStack..." + @localstack stop + @echo "LocalStack stopped successfully." + +ready: ## Wait until the LocalStack container is ready + @echo "Waiting on the LocalStack container..." + @localstack wait -t 30 && echo "LocalStack is ready to use!" || { echo "Gave up waiting on LocalStack, exiting."; exit 1; } + +logs: ## Save LocalStack logs to logs.txt + @localstack logs > logs.txt + +PKG_SUB_DIRS := $(dir $(shell find . -type d -name node_modules -prune -o -type d -name "vendor" -prune -o -type f -name package.json -print)) + +update-deps: ## Update npm dependencies in all sub-projects + for i in $(PKG_SUB_DIRS); do \ + pushd $$i && ncu -u && npm install && popd; \ + done + +.PHONY: usage check install bundle-ruby bundle-node build bootstrap deploy deploy-aws \ + destroy destroy-aws send-request start stop ready logs update-deps diff --git a/README.md b/README.md index a5bb424..535c784 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,147 @@ -[![Build Status](https://travis-ci.org/localstack/localstack-demo.svg?branch=master)](https://travis-ci.org/whummer/localstack-demo) +# Async Request Processing with Lambda and Step Functions using LocalStack -# LocalStack Demo +| Key | Value | +| ------------ | ------------------------------------------------------------------------------------------------------------ | +| Environment | LocalStack, AWS | +| Services | Lambda, DynamoDB, SQS, Step Functions, API Gateway, S3 | +| Integrations | AWS CDK, AWS CLI, Docker, LocalStack | +| Categories | Serverless, Compute | +| Level | Intermediate | +| Use Case | Asynchronous Request Processing | +| GitHub | [Repository link](https://github.com/localstack/localstack-demo) | -Simple demo application deployed using LocalStack, developed using the Serverless framework. +## Introduction -The sample app illustrates a typical Web application scenario with asynchronous request processing happening in the background, all running locally inside LocalStack. The figure below outlines the application architecture with the different components and services involved in processing the requests. +This sample demonstrates a typical web application scenario where requests are accepted by a REST API and processed asynchronously in the background — all running locally inside LocalStack. The infrastructure is defined with AWS CDK and uses three different Lambda runtimes (Node.js, Python, and Ruby) to showcase a polyglot serverless architecture. - +When a user creates a new request via the frontend, it travels through SQS, a Step Functions state machine, and two Python Lambda functions before the result is written to S3. The frontend polls the API to display live status transitions (`QUEUED → PROCESSING → FINISHED`). + +## Architecture + +The following diagram shows the architecture that this sample application builds and deploys: + +![Architecture](./demo/web/architecture.png) + +- **API Gateway (REST)** — exposes `POST /requests` and `GET /requests` endpoints backed by a Node.js Lambda function. +- **[SQS](https://docs.localstack.cloud/user-guide/aws/sqs/)** — decouples the HTTP handler from the processing pipeline; the Node.js Lambda enqueues each new request. +- **[Lambda](https://docs.localstack.cloud/user-guide/aws/lambda/)** — three runtimes in play: + - **Node.js** (`httpHandleRequest`) — handles HTTP requests, writes initial status to DynamoDB, enqueues to SQS. + - **Ruby** (`sqsHandleItem`) — consumes SQS messages and triggers the Step Functions execution. + - **Python** (`backendProcessRequest`, `backendArchiveResult`) — processes and archives results. +- **[Step Functions](https://docs.localstack.cloud/user-guide/aws/stepfunctions/)** — orchestrates the two-step processing pipeline. +- **[DynamoDB](https://docs.localstack.cloud/user-guide/aws/dynamodb/)** — stores request status at every stage. +- **[S3](https://docs.localstack.cloud/user-guide/aws/s3/)** — stores the final result file and serves the React frontend. ## Prerequisites -* LocalStack -* Docker -* Node.js / `yarn` -* `make` -* (optional) jq +- A valid [LocalStack for AWS license](https://localstack.cloud/pricing). Your license provides a [`LOCALSTACK_AUTH_TOKEN`](https://docs.localstack.cloud/getting-started/auth-token/) to activate LocalStack. +- [`localstack` CLI](https://docs.localstack.cloud/getting-started/installation/#localstack-cli). +- [AWS CLI](https://docs.localstack.cloud/user-guide/integrations/aws-cli/) with the [`awslocal` wrapper](https://docs.localstack.cloud/user-guide/integrations/aws-cli/#localstack-aws-cli-awslocal). +- [CDK](https://docs.localstack.cloud/user-guide/integrations/aws-cdk/) with the [`cdklocal`](https://www.npmjs.com/package/aws-cdk-local) wrapper (installed automatically via `cdk/package.json`). +- [Node.js 22+](https://nodejs.org/en/download/) +- [Docker](https://docs.docker.com/get-docker/) — required to bundle Ruby Lambda gems. +- [`jq`](https://jqlang.github.io/jq/download/) +- [`make`](https://www.gnu.org/software/make/) -Note: Please make sure to pull and start the `latest` LocalStack Docker image. At the time of writing (2023-02-01), the demo requires some features that were only recently added to LocalStack and are not part of a tagged release version yet. +## Installation -## Running LocalStack +Clone the repository: -Use the `localstack` CLI command to get started: +```bash +git clone https://github.com/localstack/localstack-demo.git +cd localstack-demo ``` -localstack start + +Install the CDK project dependencies: + +```bash +make install ``` -## Installing dependencies & running the application +## Deployment -To install the dependencies, deploy and start the application locally in LocalStack: +Set your LocalStack auth token and start LocalStack: + +```bash +export LOCALSTACK_AUTH_TOKEN= +make start +make ready ``` + +Deploy the full stack (bundles Lambda dependencies, bootstraps CDK, deploys, uploads frontend): + +```bash make deploy ``` +The output will be similar to the following: + +``` +LocalstackDemoStack: deploying... [1/1] +... + ✅ LocalstackDemoStack + +Outputs: +LocalstackDemoStack.ApiEndpoint = https://.execute-api.localhost.localstack.cloud:4566/local/ +LocalstackDemoStack.WebsiteUrl = http://localhost:4566/archive-bucket/index.html + +Done! Open http://localhost:4566/archive-bucket/index.html in your browser. +``` + ## Testing -After starting the app, open this URL in your browser: http://localhost:4566/archive-bucket/index.html +### Browser UI -* Enable the option "Auto-Refresh" to continuously poll for new results -* Click the button "Create new request" to send a new request to the backend API -* The new request will go through the phases `QUEUED->PROCESSING->FINISHED` as the request is being handled by the backend services (Lambda functions, Step Functions state machine) +Open the frontend in your browser: -If you have the [`awslocal`](https://github.com/localstack/awscli-local) command line installed, you can browse the contents of the local S3 bucket via: ``` +http://localhost:4566/archive-bucket/index.html +``` + +![Demo Application](./demo/web/demo.png) + +- Enable **Auto-Refresh** to continuously poll for new results. +- Click **Create new request** to send a new request to the backend API. +- Watch the request move through `QUEUED → PROCESSING → FINISHED` in the table. +- When the status is `FINISHED`, a **Download result** link appears pointing to the result file in S3. + +### CLI smoke test + +To send a request from the terminal and poll S3 until the result appears: + +```bash +make send-request +``` + +The output will be similar to the following: + +``` +Looking up REST API ID... +Sending request to API ID 'lgbmikdf4o' ... +Received request ID 'e5503b47' +Polling s3://archive-bucket/ for result ... + PRE e5503b47/ +``` + +You can also browse the contents of the archive bucket directly: + +```bash awslocal s3 ls s3://archive-bucket/ ``` -## License +## Summary + +This sample application demonstrates how to build and test a polyglot serverless pipeline using AWS CDK and LocalStack: + +- Defining AWS infrastructure (API Gateway, Lambda, SQS, Step Functions, DynamoDB, S3) entirely with **AWS CDK in TypeScript**. +- Running **three Lambda runtimes** (Node.js, Python, Ruby) side-by-side in the same CDK stack. +- Serving a **React frontend from S3** that auto-discovers the API Gateway endpoint and polls for status updates. +- Using `cdklocal` and `awslocal` to streamline **local deployment and testing** without touching real AWS. +- Providing a **GitHub Actions workflow** that runs the full integration test suite on every push. + +## Learn More -This code is available under the Apache 2.0 license. +- [LocalStack Lambda documentation](https://docs.localstack.cloud/user-guide/aws/lambda/) +- [LocalStack Step Functions documentation](https://docs.localstack.cloud/user-guide/aws/stepfunctions/) +- [Deploying AWS CDK applications with LocalStack](https://docs.localstack.cloud/user-guide/integrations/aws-cdk/) +- [AWS CDK documentation](https://docs.aws.amazon.com/cdk/v2/guide/home.html) diff --git a/cdk/bin/localstack-demo.ts b/cdk/bin/localstack-demo.ts new file mode 100644 index 0000000..799c2f1 --- /dev/null +++ b/cdk/bin/localstack-demo.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { LocalstackDemoStack } from '../lib/localstack-demo-stack'; + +const app = new cdk.App(); +new LocalstackDemoStack(app, 'LocalstackDemoStack'); diff --git a/cdk/cdk.json b/cdk/cdk.json new file mode 100644 index 0000000..9bd9f81 --- /dev/null +++ b/cdk/cdk.json @@ -0,0 +1,20 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/localstack-demo.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "node_modules" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/aws-iam:minimizePolicies": true + } +} diff --git a/cdk/dist/bin/localstack-demo.d.ts b/cdk/dist/bin/localstack-demo.d.ts new file mode 100644 index 0000000..4e7cc25 --- /dev/null +++ b/cdk/dist/bin/localstack-demo.d.ts @@ -0,0 +1,3 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +//# sourceMappingURL=localstack-demo.d.ts.map \ No newline at end of file diff --git a/cdk/dist/bin/localstack-demo.d.ts.map b/cdk/dist/bin/localstack-demo.d.ts.map new file mode 100644 index 0000000..b35ea17 --- /dev/null +++ b/cdk/dist/bin/localstack-demo.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"localstack-demo.d.ts","sourceRoot":"","sources":["../../bin/localstack-demo.ts"],"names":[],"mappings":";AACA,OAAO,6BAA6B,CAAC"} \ No newline at end of file diff --git a/cdk/dist/bin/localstack-demo.js b/cdk/dist/bin/localstack-demo.js new file mode 100644 index 0000000..6c4bab6 --- /dev/null +++ b/cdk/dist/bin/localstack-demo.js @@ -0,0 +1,41 @@ +#!/usr/bin/env node +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +require("source-map-support/register"); +const cdk = __importStar(require("aws-cdk-lib")); +const localstack_demo_stack_1 = require("../lib/localstack-demo-stack"); +const app = new cdk.App(); +new localstack_demo_stack_1.LocalstackDemoStack(app, 'LocalstackDemoStack'); diff --git a/cdk/dist/lib/localstack-demo-stack.d.ts b/cdk/dist/lib/localstack-demo-stack.d.ts new file mode 100644 index 0000000..7cb8938 --- /dev/null +++ b/cdk/dist/lib/localstack-demo-stack.d.ts @@ -0,0 +1,6 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +export declare class LocalstackDemoStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps); +} +//# sourceMappingURL=localstack-demo-stack.d.ts.map \ No newline at end of file diff --git a/cdk/dist/lib/localstack-demo-stack.d.ts.map b/cdk/dist/lib/localstack-demo-stack.d.ts.map new file mode 100644 index 0000000..348e373 --- /dev/null +++ b/cdk/dist/lib/localstack-demo-stack.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"localstack-demo-stack.d.ts","sourceRoot":"","sources":["../../lib/localstack-demo-stack.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAWvC,qBAAa,mBAAoB,SAAQ,GAAG,CAAC,KAAK;gBACpC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,CAAC,UAAU;CAqIjE"} \ No newline at end of file diff --git a/cdk/dist/lib/localstack-demo-stack.js b/cdk/dist/lib/localstack-demo-stack.js new file mode 100644 index 0000000..49da1ff --- /dev/null +++ b/cdk/dist/lib/localstack-demo-stack.js @@ -0,0 +1,167 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LocalstackDemoStack = void 0; +const cdk = __importStar(require("aws-cdk-lib")); +const lambda = __importStar(require("aws-cdk-lib/aws-lambda")); +const dynamodb = __importStar(require("aws-cdk-lib/aws-dynamodb")); +const sqs = __importStar(require("aws-cdk-lib/aws-sqs")); +const s3 = __importStar(require("aws-cdk-lib/aws-s3")); +const sfn = __importStar(require("aws-cdk-lib/aws-stepfunctions")); +const tasks = __importStar(require("aws-cdk-lib/aws-stepfunctions-tasks")); +const lambdaEventSources = __importStar(require("aws-cdk-lib/aws-lambda-event-sources")); +const apigateway = __importStar(require("aws-cdk-lib/aws-apigateway")); +const path = __importStar(require("path")); +class LocalstackDemoStack extends cdk.Stack { + constructor(scope, id, props) { + super(scope, id, props); + // ── DynamoDB ────────────────────────────────────────────────────────── + const table = new dynamodb.Table(this, 'AppRequests', { + tableName: 'appRequests', + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, + sortKey: { name: 'requestID', type: dynamodb.AttributeType.STRING }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + // ── SQS Queue ───────────────────────────────────────────────────────── + const queue = new sqs.Queue(this, 'RequestQueue', { + queueName: 'requestQueue', + visibilityTimeout: cdk.Duration.seconds(60), + }); + // ── S3 Archive Bucket ───────────────────────────────────────────────── + const archiveBucket = new s3.Bucket(this, 'ArchiveBucket', { + bucketName: 'archive-bucket', + removalPolicy: cdk.RemovalPolicy.DESTROY, + publicReadAccess: true, + blockPublicAccess: new s3.BlockPublicAccess({ + blockPublicAcls: false, + blockPublicPolicy: false, + ignorePublicAcls: false, + restrictPublicBuckets: false, + }), + }); + // ── Python Lambdas (3.13) ───────────────────────────────────────────── + const processingLambda = new lambda.Function(this, 'BackendProcessRequest', { + functionName: 'backendProcessRequest', + runtime: lambda.Runtime.PYTHON_3_13, + handler: 'processing.handle_request', + code: lambda.Code.fromAsset(path.join(__dirname, '../../demo/lambdas/python')), + timeout: cdk.Duration.seconds(30), + environment: { + TABLE_NAME: table.tableName, + }, + }); + table.grantWriteData(processingLambda); + const archiveLambda = new lambda.Function(this, 'BackendArchiveResult', { + functionName: 'backendArchiveResult', + runtime: lambda.Runtime.PYTHON_3_13, + handler: 'processing.archive_result', + code: lambda.Code.fromAsset(path.join(__dirname, '../../demo/lambdas/python')), + timeout: cdk.Duration.seconds(30), + environment: { + TABLE_NAME: table.tableName, + ARCHIVE_BUCKET: archiveBucket.bucketName, + }, + }); + table.grantWriteData(archiveLambda); + archiveBucket.grantWrite(archiveLambda); + // ── Step Functions State Machine ────────────────────────────────────── + const processTask = new tasks.LambdaInvoke(this, 'ProcessRequest', { + lambdaFunction: processingLambda, + outputPath: '$.Payload', + }); + const archiveTask = new tasks.LambdaInvoke(this, 'ArchiveResult', { + lambdaFunction: archiveLambda, + outputPath: '$.Payload', + }); + const stateMachine = new sfn.StateMachine(this, 'ProcessingStateMachine', { + stateMachineName: 'processingStateMachine', + definitionBody: sfn.DefinitionBody.fromChainable(processTask.next(archiveTask)), + timeout: cdk.Duration.minutes(5), + }); + // ── Ruby Lambda 3.3 (SQS handler) ───────────────────────────────────── + const sqsHandlerLambda = new lambda.Function(this, 'SqsHandleItem', { + functionName: 'sqsHandleItem', + runtime: lambda.Runtime.RUBY_3_3, + handler: 'worker.triggerProcessing', + code: lambda.Code.fromAsset(path.join(__dirname, '../../demo/lambdas/ruby')), + timeout: cdk.Duration.seconds(30), + environment: { + STATE_MACHINE_ARN: stateMachine.stateMachineArn, + }, + }); + stateMachine.grantStartExecution(sqsHandlerLambda); + queue.grantConsumeMessages(sqsHandlerLambda); + sqsHandlerLambda.addEventSource(new lambdaEventSources.SqsEventSource(queue, { batchSize: 1 })); + // ── Node.js Lambda 22.x (HTTP handler) ─────────────────────────────── + const httpHandlerLambda = new lambda.Function(this, 'HttpHandleRequest', { + functionName: 'httpHandleRequest', + runtime: lambda.Runtime.NODEJS_22_X, + handler: 'app.handleRequest', + code: lambda.Code.fromAsset(path.join(__dirname, '../../demo/lambdas/nodejs')), + timeout: cdk.Duration.seconds(30), + environment: { + TABLE_NAME: table.tableName, + QUEUE_URL: queue.queueUrl, + }, + }); + table.grantReadWriteData(httpHandlerLambda); + queue.grantSendMessages(httpHandlerLambda); + // ── REST API Gateway (stage=local to match frontend discovery) ──────── + const api = new apigateway.RestApi(this, 'LocalstackDemoApi', { + restApiName: 'localstack-demo', + deployOptions: { stageName: 'local' }, + defaultCorsPreflightOptions: { + allowOrigins: apigateway.Cors.ALL_ORIGINS, + allowMethods: apigateway.Cors.ALL_METHODS, + allowHeaders: apigateway.Cors.DEFAULT_HEADERS, + }, + }); + const requests = api.root.addResource('requests'); + const integration = new apigateway.LambdaIntegration(httpHandlerLambda); + requests.addMethod('POST', integration); + requests.addMethod('GET', integration); + // ── Outputs ─────────────────────────────────────────────────────────── + new cdk.CfnOutput(this, 'ApiEndpoint', { + value: api.url, + description: 'REST API Gateway endpoint URL', + }); + new cdk.CfnOutput(this, 'WebsiteUrl', { + value: 'http://localhost:4566/archive-bucket/index.html', + description: 'Frontend URL — open this in your browser after deploy', + }); + } +} +exports.LocalstackDemoStack = LocalstackDemoStack; diff --git a/cdk/lib/localstack-demo-stack.ts b/cdk/lib/localstack-demo-stack.ts new file mode 100644 index 0000000..a2a04fe --- /dev/null +++ b/cdk/lib/localstack-demo-stack.ts @@ -0,0 +1,147 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as sqs from 'aws-cdk-lib/aws-sqs'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as sfn from 'aws-cdk-lib/aws-stepfunctions'; +import * as tasks from 'aws-cdk-lib/aws-stepfunctions-tasks'; +import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources'; +import * as apigateway from 'aws-cdk-lib/aws-apigateway'; +import * as path from 'path'; + +export class LocalstackDemoStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // ── DynamoDB ────────────────────────────────────────────────────────── + const table = new dynamodb.Table(this, 'AppRequests', { + tableName: 'appRequests', + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, + sortKey: { name: 'requestID', type: dynamodb.AttributeType.STRING }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + // ── SQS Queue ───────────────────────────────────────────────────────── + const queue = new sqs.Queue(this, 'RequestQueue', { + queueName: 'requestQueue', + visibilityTimeout: cdk.Duration.seconds(60), + }); + + // ── S3 Archive Bucket ───────────────────────────────────────────────── + const archiveBucket = new s3.Bucket(this, 'ArchiveBucket', { + bucketName: 'archive-bucket', + removalPolicy: cdk.RemovalPolicy.DESTROY, + publicReadAccess: true, + blockPublicAccess: new s3.BlockPublicAccess({ + blockPublicAcls: false, + blockPublicPolicy: false, + ignorePublicAcls: false, + restrictPublicBuckets: false, + }), + }); + + // ── Python Lambdas (3.13) ───────────────────────────────────────────── + const processingLambda = new lambda.Function(this, 'BackendProcessRequest', { + functionName: 'backendProcessRequest', + runtime: lambda.Runtime.PYTHON_3_13, + handler: 'processing.handle_request', + code: lambda.Code.fromAsset(path.join(__dirname, '../../demo/lambdas/python')), + timeout: cdk.Duration.seconds(30), + environment: { + TABLE_NAME: table.tableName, + }, + }); + table.grantWriteData(processingLambda); + + const archiveLambda = new lambda.Function(this, 'BackendArchiveResult', { + functionName: 'backendArchiveResult', + runtime: lambda.Runtime.PYTHON_3_13, + handler: 'processing.archive_result', + code: lambda.Code.fromAsset(path.join(__dirname, '../../demo/lambdas/python')), + timeout: cdk.Duration.seconds(30), + environment: { + TABLE_NAME: table.tableName, + ARCHIVE_BUCKET: archiveBucket.bucketName, + }, + }); + table.grantWriteData(archiveLambda); + archiveBucket.grantWrite(archiveLambda); + + // ── Step Functions State Machine ────────────────────────────────────── + const processTask = new tasks.LambdaInvoke(this, 'ProcessRequest', { + lambdaFunction: processingLambda, + outputPath: '$.Payload', + }); + + const archiveTask = new tasks.LambdaInvoke(this, 'ArchiveResult', { + lambdaFunction: archiveLambda, + outputPath: '$.Payload', + }); + + const stateMachine = new sfn.StateMachine(this, 'ProcessingStateMachine', { + stateMachineName: 'processingStateMachine', + definitionBody: sfn.DefinitionBody.fromChainable(processTask.next(archiveTask)), + timeout: cdk.Duration.minutes(5), + }); + + // ── Ruby Lambda 3.3 (SQS handler) ───────────────────────────────────── + const sqsHandlerLambda = new lambda.Function(this, 'SqsHandleItem', { + functionName: 'sqsHandleItem', + runtime: lambda.Runtime.RUBY_3_3, + handler: 'worker.triggerProcessing', + code: lambda.Code.fromAsset(path.join(__dirname, '../../demo/lambdas/ruby')), + timeout: cdk.Duration.seconds(30), + environment: { + STATE_MACHINE_ARN: stateMachine.stateMachineArn, + }, + }); + stateMachine.grantStartExecution(sqsHandlerLambda); + queue.grantConsumeMessages(sqsHandlerLambda); + sqsHandlerLambda.addEventSource( + new lambdaEventSources.SqsEventSource(queue, { batchSize: 1 }), + ); + + // ── Node.js Lambda 22.x (HTTP handler) ─────────────────────────────── + const httpHandlerLambda = new lambda.Function(this, 'HttpHandleRequest', { + functionName: 'httpHandleRequest', + runtime: lambda.Runtime.NODEJS_22_X, + handler: 'app.handleRequest', + code: lambda.Code.fromAsset(path.join(__dirname, '../../demo/lambdas/nodejs')), + timeout: cdk.Duration.seconds(30), + environment: { + TABLE_NAME: table.tableName, + QUEUE_URL: queue.queueUrl, + }, + }); + table.grantReadWriteData(httpHandlerLambda); + queue.grantSendMessages(httpHandlerLambda); + + // ── REST API Gateway (stage=local to match frontend discovery) ──────── + const api = new apigateway.RestApi(this, 'LocalstackDemoApi', { + restApiName: 'localstack-demo', + deployOptions: { stageName: 'local' }, + defaultCorsPreflightOptions: { + allowOrigins: apigateway.Cors.ALL_ORIGINS, + allowMethods: apigateway.Cors.ALL_METHODS, + allowHeaders: apigateway.Cors.DEFAULT_HEADERS, + }, + }); + + const requests = api.root.addResource('requests'); + const integration = new apigateway.LambdaIntegration(httpHandlerLambda); + requests.addMethod('POST', integration); + requests.addMethod('GET', integration); + + // ── Outputs ─────────────────────────────────────────────────────────── + new cdk.CfnOutput(this, 'ApiEndpoint', { + value: api.url, + description: 'REST API Gateway endpoint URL', + }); + new cdk.CfnOutput(this, 'WebsiteUrl', { + value: 'http://localhost:4566/archive-bucket/index.html', + description: 'Frontend URL — open this in your browser after deploy', + }); + } +} diff --git a/cdk/package-lock.json b/cdk/package-lock.json new file mode 100644 index 0000000..14deee4 --- /dev/null +++ b/cdk/package-lock.json @@ -0,0 +1,719 @@ +{ + "name": "localstack-demo-cdk", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "localstack-demo-cdk", + "version": "1.0.0", + "dependencies": { + "aws-cdk-lib": "^2.180.0", + "constructs": "^10.4.2", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "@types/node": "^22.13.0", + "aws-cdk": "^2.1111.0", + "aws-cdk-local": "^2.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.7.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==" + }, + "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==" + }, + "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" + ], + "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, + "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, + "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 + }, + "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, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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, + "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, + "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, + "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 + }, + "node_modules/aws-cdk": { + "version": "2.1111.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1111.0.tgz", + "integrity": "sha512-69AVF04cxbAhYzmeJYtUF5bs6ruNnH05EQZJfjadk5Lpg+HVaJY2NjG/2qgsMmKrfRgvLet73Ir8ehVlAaaGng==", + "dev": true, + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.243.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.243.0.tgz", + "integrity": "sha512-qIhg/3gSNeZ9LoVmDATO45HPk+POkoCfPZRezeOPhd2kAJ/wzYswyUcMqpDWXrlRrEVYntxsykQs+2eMA04Isg==", + "bundleDependencies": [ + "@balena/dockerignore", + "@aws-cdk/cloud-assembly-api", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "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/aws-cdk-local": { + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/aws-cdk-local/-/aws-cdk-local-2.19.2.tgz", + "integrity": "sha512-1aT+yD4bN23Zgqjyd7AbLOFTK04xM8L7GBWPdAridqG20cyptnDCmRY7iYwWPx19LnJAA0HaiTxoWQTxDwuFLA==", + "dev": true, + "dependencies": { + "diff": "^5.0.0" + }, + "bin": { + "cdklocal": "bin/cdklocal" + } + }, + "node_modules/aws-cdk-local/node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "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==" + }, + "node_modules/constructs": { + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.5.1.tgz", + "integrity": "sha512-f/TfFXiS3G/yVIXDjOQn9oTlyu9Wo7Fxyjj7lb8r92iO81jR2uST+9MstxZTmDGx/CgIbxCXkFXgupnLTNxQZg==" + }, + "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 + }, + "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, + "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 + }, + "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==", + "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==", + "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, + "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, + "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 + }, + "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 + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/cdk/package.json b/cdk/package.json new file mode 100644 index 0000000..4844f51 --- /dev/null +++ b/cdk/package.json @@ -0,0 +1,21 @@ +{ + "name": "localstack-demo-cdk", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tsc", + "cdk": "cdk" + }, + "dependencies": { + "aws-cdk-lib": "^2.180.0", + "constructs": "^10.4.2", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "@types/node": "^22.13.0", + "aws-cdk": "^2.1111.0", + "aws-cdk-local": "^2.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.7.0" + } +} diff --git a/cdk/tsconfig.json b/cdk/tsconfig.json new file mode 100644 index 0000000..9350d6c --- /dev/null +++ b/cdk/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["es2020"], + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist", + "rootDir": ".", + "declaration": true, + "declarationMap": true + }, + "include": ["bin/**/*", "lib/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/demo/lambdas/nodejs/app.js b/demo/lambdas/nodejs/app.js new file mode 100644 index 0000000..b46b003 --- /dev/null +++ b/demo/lambdas/nodejs/app.js @@ -0,0 +1,93 @@ +'use strict'; + +const { DynamoDBClient, PutItemCommand, ScanCommand } = require('@aws-sdk/client-dynamodb'); +const { SQSClient, GetQueueUrlCommand, SendMessageCommand } = require('@aws-sdk/client-sqs'); +const { randomUUID } = require('crypto'); + +// AWS SDK v3 automatically picks up AWS_ENDPOINT_URL from the environment +const dynamoClient = new DynamoDBClient({}); +const sqsClient = new SQSClient({}); + +const TABLE_NAME = process.env.TABLE_NAME || 'appRequests'; +const QUEUE_URL = process.env.QUEUE_URL; +const QUEUE_NAME = process.env.QUEUE_NAME || 'requestQueue'; + +const shortUid = () => randomUUID().substring(0, 8); + +const headers = { + 'content-type': 'application/json', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'OPTIONS,POST,GET', +}; + +const handleRequest = async (event) => { + const reqPath = event.path || event.rawPath || ''; + const method = + event.httpMethod || + (event.requestContext && event.requestContext.http && event.requestContext.http.method) || + ''; + + if (reqPath.endsWith('/requests') && method === 'POST') { + return startNewRequest(); + } else if (reqPath.endsWith('/requests') && method === 'GET') { + return listRequests(); + } + return { statusCode: 404, headers, body: JSON.stringify({}) }; +}; + +const startNewRequest = async () => { + const requestID = shortUid(); + + // Resolve queue URL — prefer the injected env var, fall back to lookup + let queueUrl = QUEUE_URL; + if (!queueUrl) { + const res = await sqsClient.send(new GetQueueUrlCommand({ QueueName: QUEUE_NAME })); + queueUrl = res.QueueUrl; + } + + await sqsClient.send( + new SendMessageCommand({ + QueueUrl: queueUrl, + MessageBody: JSON.stringify({ requestID }), + }), + ); + + await dynamoClient.send( + new PutItemCommand({ + TableName: TABLE_NAME, + Item: { + id: { S: shortUid() }, + requestID: { S: requestID }, + timestamp: { N: String(Date.now()) }, + status: { S: 'QUEUED' }, + }, + }), + ); + + return { + statusCode: 200, + headers, + body: JSON.stringify({ requestID, status: 'QUEUED' }), + }; +}; + +const listRequests = async () => { + const result = await dynamoClient.send(new ScanCommand({ TableName: TABLE_NAME })); + const items = (result.Items || []).map((item) => { + const obj = {}; + for (const [key, val] of Object.entries(item)) { + if (val.N !== undefined) obj[key] = parseFloat(val.N); + else if (val.S !== undefined) obj[key] = val.S; + else obj[key] = Object.values(val)[0]; + } + return obj; + }); + return { + statusCode: 200, + headers, + body: JSON.stringify({ result: items }), + }; +}; + +module.exports = { handleRequest }; diff --git a/demo/lambdas/nodejs/package-lock.json b/demo/lambdas/nodejs/package-lock.json new file mode 100644 index 0000000..88bc655 --- /dev/null +++ b/demo/lambdas/nodejs/package-lock.json @@ -0,0 +1,1348 @@ +{ + "name": "localstack-demo-nodejs-lambda", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "localstack-demo-nodejs-lambda", + "version": "1.0.0", + "dependencies": { + "@aws-sdk/client-dynamodb": "^3.758.0", + "@aws-sdk/client-sqs": "^3.758.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb": { + "version": "3.1011.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.1011.0.tgz", + "integrity": "sha512-oCYlsiLR0qESxmr6LWeUDZH2qITGL69mgFxqFPzrblBfKSZPw6jEzVSB/T6JUqzSQrARnusiLNHxn6eph0rSHQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/dynamodb-codec": "^3.972.21", + "@aws-sdk/middleware-endpoint-discovery": "^3.972.8", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@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.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.13", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-sqs": { + "version": "3.1011.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1011.0.tgz", + "integrity": "sha512-PPNHwT3737xmHqC3liSIHYl0sE23UYvnXnajvhNmFlOmjeZthzi1ZiYcuuoSwyaFUX1gInxv+/C8NdfmZcdsfg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-sdk-sqs": "^3.972.15", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/md5-js": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@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.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.973.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.20.tgz", + "integrity": "sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/xml-builder": "^3.972.11", + "@smithy/core": "^3.23.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.18.tgz", + "integrity": "sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.20.tgz", + "integrity": "sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.19", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.20.tgz", + "integrity": "sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-login": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.20.tgz", + "integrity": "sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.21.tgz", + "integrity": "sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-ini": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.18.tgz", + "integrity": "sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.20.tgz", + "integrity": "sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/token-providers": "3.1009.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.20.tgz", + "integrity": "sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/dynamodb-codec": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.21.tgz", + "integrity": "sha512-6wsIKQWJx87F1SZyQ/SfV7ovdvP0R2l5vpgSxT1+b9Qmx2IYnvWNNJfmpd3HJRN7aokEh/IV/eFlVnsZF2NXCQ==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@smithy/core": "^3.23.11", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/endpoint-cache": { + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.972.4.tgz", + "integrity": "sha512-GdASDnWanLnHxKK0hqV97xz23QmfA/C8yGe0PiuEmWiHSe+x+x+mFEj4sXqx9IbfyPncWz8f4EhNwBSG9cgYCg==", + "dependencies": { + "mnemonist": "0.38.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-endpoint-discovery": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.8.tgz", + "integrity": "sha512-S0oXx1QbSpMDBMJn4P0hOxW8ieGAdRT+G9NbL+ESWkkoCGf9D++fKYD2fyBGtIy88OrP7wgECpXgGLAcGpIj0A==", + "dependencies": { + "@aws-sdk/endpoint-cache": "^3.972.4", + "@aws-sdk/types": "^3.973.6", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", + "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", + "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", + "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-sqs": { + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.15.tgz", + "integrity": "sha512-X7yt+gJzZEK247nppuUVWS1i83q8zhZdBk1H2b6/qeXNv1ILgw0bQLNbFNG4gJi3P7vZV+PhtPkax0nwXAvRtg==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.21.tgz", + "integrity": "sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@smithy/core": "^3.23.11", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-retry": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.996.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.10.tgz", + "integrity": "sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@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.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", + "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1009.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1009.0.tgz", + "integrity": "sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", + "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.7.tgz", + "integrity": "sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.11.tgz", + "integrity": "sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==", + "dependencies": { + "@smithy/types": "^4.13.1", + "fast-xml-parser": "5.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", + "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.11.tgz", + "integrity": "sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.23.12", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", + "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.20", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", + "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", + "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", + "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", + "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.12.tgz", + "integrity": "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", + "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.26", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.26.tgz", + "integrity": "sha512-8Qfikvd2GVKSm8S6IbjfwFlRY9VlMrj0Dp4vTwAuhqbX7NhJKE5DQc2bnfJIcY0B+2YKMDBWfvexbSZeejDgeg==", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-middleware": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.43", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.43.tgz", + "integrity": "sha512-ZwsifBdyuNHrFGmbc7bAfP2b54+kt9J2rhFd18ilQGAB+GDiP4SrawqyExbB7v455QVR7Psyhb2kjULvBPIhvA==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", + "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", + "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", + "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", + "dependencies": { + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", + "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", + "dependencies": { + "@smithy/abort-controller": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", + "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", + "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", + "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", + "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", + "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", + "dependencies": { + "@smithy/types": "^4.13.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", + "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", + "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.6.tgz", + "integrity": "sha512-aib3f0jiMsJ6+cvDnXipBsGDL7ztknYSVqJs1FdN9P+u9tr/VzOR7iygSh6EUOdaBeMCMSh3N0VdyYsG4o91DQ==", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/middleware-endpoint": "^4.4.26", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.20", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", + "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", + "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", + "dependencies": { + "@smithy/querystring-parser": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.42", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.42.tgz", + "integrity": "sha512-0vjwmcvkWAUtikXnWIUOyV6IFHTEeQUYh3JUZcDgcszF+hD/StAsQ3rCZNZEPHgI9kVNcbnyc8P2CBHnwgmcwg==", + "dependencies": { + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.45", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.45.tgz", + "integrity": "sha512-q5dOqqfTgUcLe38TAGiFn9srToKj2YCHJ34QGOLzM+xYLLA+qRZv7N+33kl1MERVusue36ZHnlNaNEvY/PzSrw==", + "dependencies": { + "@smithy/config-resolver": "^4.4.11", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", + "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", + "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", + "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", + "dependencies": { + "@smithy/service-error-classification": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.20", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", + "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.13.tgz", + "integrity": "sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ==", + "dependencies": { + "@smithy/abort-controller": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==" + }, + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/mnemonist": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", + "dependencies": { + "obliterator": "^1.6.1" + } + }, + "node_modules/obliterator": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==" + }, + "node_modules/path-expression-matcher": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", + "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/strnum": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } +} diff --git a/demo/lambdas/nodejs/package.json b/demo/lambdas/nodejs/package.json new file mode 100644 index 0000000..f1cd4c0 --- /dev/null +++ b/demo/lambdas/nodejs/package.json @@ -0,0 +1,9 @@ +{ + "name": "localstack-demo-nodejs-lambda", + "version": "1.0.0", + "private": true, + "dependencies": { + "@aws-sdk/client-dynamodb": "^3.758.0", + "@aws-sdk/client-sqs": "^3.758.0" + } +} diff --git a/demo/lambdas/python/processing.py b/demo/lambdas/python/processing.py new file mode 100644 index 0000000..8fe6f37 --- /dev/null +++ b/demo/lambdas/python/processing.py @@ -0,0 +1,59 @@ +import datetime +import os +import time +import uuid + +import boto3 + +# boto3 automatically picks up AWS_ENDPOINT_URL from the environment (boto3 >= 1.28) +DYNAMODB_TABLE = os.environ.get('TABLE_NAME') or 'appRequests' +S3_BUCKET = os.environ.get('ARCHIVE_BUCKET') or 'archive-bucket' + + +def handle_request(event, context=None): + # simulate queueing delay + time.sleep(5) + print('handle_request', event) + set_status(event['requestID'], 'PROCESSING') + # simulate processing delay + time.sleep(4) + return { + 'requestID': event['requestID'], + 'status': 'PROCESSING', + } + + +def archive_result(event, context=None): + print('archive_result', event) + requestID = event['requestID'] + s3 = boto3.client('s3') + s3.put_object( + Bucket=S3_BUCKET, + Key=f'{requestID}/result.txt', + Body=f'Archive result for request {requestID}', + ) + # simulate archive delay + time.sleep(3) + set_status(requestID, 'FINISHED') + + +def set_status(requestID, status): + dynamodb = boto3.client('dynamodb') + dynamodb.put_item( + TableName=DYNAMODB_TABLE, + Item={ + 'id': {'S': short_uid()}, + 'requestID': {'S': requestID}, + 'timestamp': {'N': str(now_utc())}, + 'status': {'S': status}, + }, + ) + + +def now_utc(): + diff = datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1) + return int(diff.total_seconds() * 1000.0) + + +def short_uid(): + return str(uuid.uuid4())[0:8] diff --git a/demo/lambdas/ruby/Gemfile b/demo/lambdas/ruby/Gemfile new file mode 100644 index 0000000..a4b1206 --- /dev/null +++ b/demo/lambdas/ruby/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gem 'aws-sdk-states', '~> 1' diff --git a/demo/lambdas/ruby/Gemfile.lock b/demo/lambdas/ruby/Gemfile.lock new file mode 100644 index 0000000..a60610d --- /dev/null +++ b/demo/lambdas/ruby/Gemfile.lock @@ -0,0 +1,32 @@ +GEM + remote: https://rubygems.org/ + specs: + aws-eventstream (1.4.0) + aws-partitions (1.1227.0) + aws-sdk-core (3.243.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-states (1.104.0) + aws-sdk-core (~> 3, >= 3.241.4) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + base64 (0.3.0) + bigdecimal (4.0.1) + jmespath (1.6.2) + logger (1.7.0) + +PLATFORMS + aarch64-linux + ruby + +DEPENDENCIES + aws-sdk-states (~> 1) + +BUNDLED WITH + 2.5.22 diff --git a/demo/lambdas/ruby/worker.rb b/demo/lambdas/ruby/worker.rb new file mode 100644 index 0000000..87e97fd --- /dev/null +++ b/demo/lambdas/ruby/worker.rb @@ -0,0 +1,21 @@ +require 'bundler/setup' +require 'aws-sdk-states' + +$state_machine_arn = ENV['STATE_MACHINE_ARN'] +$aws_endpoint_url = ENV['AWS_ENDPOINT_URL'] + +def triggerProcessing(event:, context:) + client_opts = {} + client_opts[:endpoint] = $aws_endpoint_url if $aws_endpoint_url + + client = Aws::States::Client.new(**client_opts) + + event['Records'].each do |rec| + client.start_execution( + state_machine_arn: $state_machine_arn, + input: rec['body'], + ) + end + + {} +end diff --git a/demo/web/demo-app.png b/demo/web/demo-app.png new file mode 100644 index 0000000..be6355e Binary files /dev/null and b/demo/web/demo-app.png differ diff --git a/demo/web/index.html b/demo/web/index.html index 83e31cd..e9c8a50 100644 --- a/demo/web/index.html +++ b/demo/web/index.html @@ -2,14 +2,18 @@ - - - - - - + LocalStack Demo + + + + + @@ -18,55 +22,98 @@ const refreshInterval = 5000; const bucketName = 'archive-bucket'; const region = 'us-east-1'; + + // When served from LocalStack S3 (http://localhost:4566/...) the origin IS the endpoint. + // Allow overriding via ?endpoint=... query param for local dev. const urlParams = new URLSearchParams(window.location.search); - const defaultEndpoint = 'https://localhost.localstack.cloud:4566'; - const apiEndpoint = urlParams.get('endpoint') || (window.location.port == 3000 ? defaultEndpoint : window.location.origin); - const headers = {Authorization: `AWS4-HMAC-SHA256 Credential=testkey/date/${region}/apigateway/aws4_request`}; + const apiEndpoint = urlParams.get('endpoint') || 'http://localhost:4566'; + + // Authorization header required by LocalStack for the /restapis management call + const mgmtHeaders = { + Authorization: `AWS4-HMAC-SHA256 Credential=test/20240101/${region}/apigateway/aws4_request`, + }; class App extends React.Component { - state = {} - getBaseURL() { - if(this.state.endpoint) return this.state.endpoint; - if(this.baseURL) return this.baseURL; - axios.get(`${apiEndpoint}/restapis`, {headers}).then((res) => { - const apiID = res?.data?.item?.[0]?.id; - if (apiID) { - this.baseURL = `${apiEndpoint}/restapis/${apiID}/local/_user_request_`; - this.setState(prevState=>({...prevState})); - } - }); + state = { baseURL: null, autoUpdate: false, discovering: true } + + componentDidMount() { + // Check for manual endpoint override first + const manual = urlParams.get('api'); + if (manual) { + this.setState({ baseURL: manual, discovering: false }); + return; + } + this.discoverEndpoint(); + } + + discoverEndpoint() { + axios.get(`${apiEndpoint}/restapis`, { headers: mgmtHeaders }) + .then((res) => { + const apiID = res?.data?.item?.[0]?.id; + if (apiID) { + // Use the new LocalStack execute-api path (replaces deprecated _user_request_) + const baseURL = `${apiEndpoint}/_aws/execute-api/${apiID}/local`; + this.setState({ baseURL, discovering: false }); + } else { + this.setState({ discovering: false }); + } + }) + .catch(() => this.setState({ discovering: false })); } + sendRequest() { - const baseURL = this.getBaseURL(); - if(!baseURL) return; - const url = baseURL + '/requests'; - axios.post(url, '{}', {headers}).then((res) => { - alert('New request has been sent to the API. It should appear in the list as QUEUED shortly.'); - this.setState({autoUpdate: true}); - }); + const { baseURL } = this.state; + if (!baseURL) { + alert('API endpoint not yet discovered — please wait a moment and try again.'); + return; + } + axios.post(`${baseURL}/requests`, '{}', { headers: mgmtHeaders }) + .then(() => { + alert('New request sent! It will appear as QUEUED shortly.'); + this.setState({ autoUpdate: true }); + }) + .catch((err) => alert(`Request failed: ${err.message}`)); } + loopRefresh() { if (!this.state.autoUpdate) return; - this.setState(prevState=>({...prevState})); + this.forceUpdate(); setTimeout(this.loopRefresh.bind(this), refreshInterval); } + changeAutoUpdate() { - this.setState({autoUpdate: !this.state.autoUpdate}); - setTimeout(this.loopRefresh.bind(this), 1); - } - changeEndpoint(event) { - this.setState({endpoint: event.target.value}); + const next = !this.state.autoUpdate; + this.setState({ autoUpdate: next }); + if (next) setTimeout(this.loopRefresh.bind(this), 1); } + render() { + const { baseURL, discovering, autoUpdate } = this.state; return (

Requests:

-
Auto-Refresh
-
API Gateway API endpoint: (leave empty for auto-discovery)
-
+ {discovering &&

Discovering API endpoint…

} + {!discovering && !baseURL && ( +

+ Could not discover API Gateway. Make sure LocalStack is running and the stack is deployed. +

+ )} +
+ {' '} + Auto-Refresh +
+
+ +
- - + + + + + +
TimestampRequest IDStatusAction
TimestampRequest IDStatusAction
); @@ -75,48 +122,64 @@

Requests:

class RequestEvents extends React.Component { shouldFetchData() { - if(!this.props.baseURL || (this.nextUpdateTime && !this.props.autoUpdate)) return false; - if(this.nextUpdateTime && this.nextUpdateTime > Date.now()) return false; + if (!this.props.baseURL) return false; + if (this.nextUpdateTime && !this.props.autoUpdate) return false; + if (this.nextUpdateTime && this.nextUpdateTime > Date.now()) return false; this.nextUpdateTime = Date.now() + refreshInterval; return true; } - componentDidUpdate() { - if(!this.shouldFetchData()) return; - const url = this.props.baseURL + '/requests'; - axios.get(url).then((res) => { - this.requests = res.data.result; - this.setState(prevState=>({...prevState})); - }); + + componentDidMount() { + if (this.props.baseURL) this.fetchRequests(); + } + + componentDidUpdate(prevProps) { + if (prevProps.baseURL !== this.props.baseURL && this.props.baseURL) { + this.fetchRequests(); + return; + } + if (!this.shouldFetchData()) return; + this.fetchRequests(); + } + + fetchRequests() { + axios.get(`${this.props.baseURL}/requests`) + .then((res) => { + this.requests = res.data.result; + this.setState({}); + }) + .catch((err) => console.error('Failed to fetch requests:', err)); } + render() { - return (this.requests || []). - sort((r1, r2) => r2.timestamp - r1.timestamp). - map((r) => ); + return (this.requests || []) + .sort((a, b) => b.timestamp - a.timestamp) + .map((r) => ); } } class Request extends React.Component { render() { const { req } = this.props; + const statusClass = `status-${req.status?.toLowerCase()}`; return ( {new Date(req.timestamp).toLocaleString()} - {req.requestID} - {req.status} + {req.requestID} + {req.status} - {req.status === 'FINISHED' && - - Download result from S3 - } + {req.status === 'FINISHED' && ( + + Download result + + )} ); } } - const container = document.getElementById('root'); - // Create a root - const root = ReactDOM.createRoot(container); - // Initial render + + const root = ReactDOM.createRoot(document.getElementById('root')); root.render(React.createElement(App));
@@ -124,25 +187,26 @@

Requests:

Request Worker Sample App

- This sample app illustrates a typical Web application scenario with asynchronous request processing happening in the background, all running locally inside LocalStack. -

- The end-to-end process is illustrated in the figure on the right. -

- Here's how the app works: + This sample app illustrates a typical Web application scenario with asynchronous + request processing happening in the background, all running locally inside LocalStack.

+

The end-to-end process is illustrated in the figure on the right.

+

Here's how the app works:

    -
  • - Use the button above to send a new request to local API Gateway, which will then be handled by a Lambda function for further processing. Enable "Auto-Refresh" to poll the status of the request. -
  • - As the request is going through different stages of processing, the table will be updated with the current request status (QUEUED -> PROCESSING -> FINISHED). -
  • - Once the request is FINISHED, the processing result will become available as a link to a file stored in S3. -
  • +
  • Use the button above to send a new request to local API Gateway, which will then be + handled by a Lambda function for further processing. Enable "Auto-Refresh" to poll + the status of the request.
  • +
  • As the request is going through different stages of processing, the table will be + updated with the current request status (QUEUED → PROCESSING → FINISHED).
  • +
  • Once the request is FINISHED, the processing result will become available as a + link to a file stored in S3.
+

(Note that all resources will be deployed to the local "us-east-1" region.)

- (Note that all resources will be deployed to the local "us-east-1" region.) -

- The full source code of this sample is available in this Github repo: https://github.com/localstack/localstack-demo + The full source code of this sample is available on GitHub: + + https://github.com/localstack/localstack-demo +

@@ -151,4 +215,3 @@

Request Worker Sample App

- diff --git a/serverless.yml b/serverless.yml deleted file mode 100644 index cc190dc..0000000 --- a/serverless.yml +++ /dev/null @@ -1,162 +0,0 @@ -service: localstack-demo - -plugins: - - serverless-deployment-bucket - - serverless-localstack - # Note: Although there's some more popular S3 sync plugins out there, most of them failed on LocalStack with: - # "Error: Non-file stream objects are not supported with SigV4" - # at Object.computeSha256 (node_modules/aws-sdk/lib/util.js:754:23) - - serverless-sync-s3 - -provider: - name: aws - stage: ${opt:stage,'local'} - region: us-east-1 - stackName: demo1 - timeout: 15 - deploymentBucket: - name: ${self:custom.deploymentBucket.${self:provider.stage}} - iam: - role: - statements: - - Effect: Allow - Action: - - dynamodb:* - Resource: 'arn:aws:dynamodb:us-east-1:*:table/appRequests' - - Effect: Allow - Action: - - sqs:* - Resource: 'arn:aws:sqs:us-east-1:*:requestQueue' - - Effect: Allow - Action: - - states:* - Resource: 'arn:aws:states:us-east-1:*:stateMachine:*' - - Effect: Allow - Action: - - s3:* - Resource: !Sub 'arn:aws:s3:::${self:custom.archiveBucket.${self:provider.stage}}/*' - -package: - excludeDevDependencies: true - exclude: - - ./** - - "!demo/**" - - "!node_modules/uuid/**" - -custom: - region: us-east-1 - accountID: '000000000000' - localstack: - stages: [local] - host: http://127.0.0.1 - debug: true - # Note: enable this configuration to automatically start up a LocalStack container in the background -# autostart: true - # lambda: - # mountCode: true - deploymentBucket: - local: localstack-test-bucket - aws: localstack-test-bucket-53194 - archiveBucket: - local: archive-bucket - aws: localstack-demo-archive-bucket-53194 - syncS3: - - bucketName: ${self:custom.archiveBucket.${self:provider.stage}} - localDir: demo/web - -functions: - httpHandleRequest: - handler: demo/lambdas/app.handleRequest - runtime: nodejs14.x - events: - - http: - path: /requests - method: post - cors: true - - http: - path: /requests - method: get - cors: true - sqsHandleItem: - handler: demo/lambdas/worker.triggerProcessing - runtime: ruby2.7 - environment: - STATE_MACHINE_ARN: !Sub '${processingStateMachine.Arn}' - events: - - sqs: - arn: - Fn::GetAtt: [requestQueue, Arn] - backendProcessRequest: - handler: demo/lambdas/processing.handle_request - runtime: python3.7 - backendArchiveResult: - handler: demo/lambdas/processing.archive_result - runtime: python3.7 - environment: - ARCHIVE_BUCKET: ${self:custom.archiveBucket.${self:provider.stage}} - -resources: - Resources: - appDatabase: - Type: AWS::DynamoDB::Table - Properties: - TableName: appRequests - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: id - AttributeType: S - - AttributeName: requestID - AttributeType: S - KeySchema: - - AttributeName: id - KeyType: HASH - - AttributeName: requestID - KeyType: RANGE - archiveBucket: - Type: AWS::S3::Bucket - Properties: - BucketName: ${self:custom.archiveBucket.${self:provider.stage}} - requestQueue: - Type: AWS::SQS::Queue - Properties: - QueueName: requestQueue - processingStateMachine: - Type: AWS::StepFunctions::StateMachine - Properties: - StateMachineName: processingStateMachine - RoleArn: !Sub '${processingStateMachineRole.Arn}' - DefinitionString: !Sub | - { - "StartAt": "ProcessRequest", - "States": { - "ProcessRequest": { - "Type": "Task", - "Resource": "${BackendProcessRequestLambdaFunction.Arn}", - "Next": "ArchiveResult" - }, - "ArchiveResult": { - "Type": "Task", - "Resource": "${BackendArchiveResultLambdaFunction.Arn}", - "End": true - } - } - } - processingStateMachineRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: !Sub 'states.${AWS::Region}.amazonaws.com' - Action: 'sts:AssumeRole' - Policies: - - PolicyName: lambda - PolicyDocument: - Statement: - - Effect: Allow - Action: 'lambda:InvokeFunction' - Resource: - - !Sub '${BackendProcessRequestLambdaFunction.Arn}' - - !Sub '${BackendArchiveResultLambdaFunction.Arn}'