├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── apidocs └── server.js ├── app.py ├── cdk.json ├── checkov.yaml ├── docs └── assets │ ├── api-gateway-routes.png │ ├── cdk-deploy-screenshot.png │ ├── cdk-destroy-screenshot.png │ ├── cdk-stacks-screenshot.png │ ├── solution-architecture.drawio │ ├── solution-architecture.png │ └── swagger-ui-screenshot.png ├── examples ├── test_greeting_api.sh └── test_ping_api.sh ├── package-lock.json ├── package.json ├── requirements.txt ├── setup.py ├── source.bat ├── stacks ├── __init__.py ├── apigateway_dynamic_publish.py └── resources │ ├── api_creation │ ├── api_creator.py │ ├── api_definition.yaml │ └── requirements.txt │ └── api_integrations │ ├── greeting.py │ └── ping.py ├── tests ├── __init__.py └── test_apigateway_dynamic_publish.py └── view_api_docs.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 132 | # VSCode extension 133 | .vscode/ 134 | /.favorites.json 135 | 136 | # TypeScript incremental build states 137 | *.tsbuildinfo 138 | 139 | # Local state files & OS specifics 140 | .DS_Store 141 | node_modules/ 142 | lerna-debug.log 143 | dist/ 144 | pack/ 145 | .BUILD_COMPLETED 146 | .local-npm/ 147 | .tools/ 148 | coverage/ 149 | .nyc_output 150 | .LAST_BUILD 151 | *.sw[a-z] 152 | *~ 153 | .idea 154 | 155 | # We don't want tsconfig at the root 156 | /tsconfig.json 157 | 158 | # CDK Context & Staging files 159 | cdk.context.json 160 | .cdk.staging/ 161 | cdk.out/ 162 | 163 | 164 | ######################## -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # api-gateway-dynamic-publish 2 | 3 | CDK project that leverages an OpenAPI definition to define, document and create an Amazon API Gateway deployment. At deploy time, a [CloudFormation Custom Resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) is leveraged to dynamically substitute the Lambda integration function ARNs into the OpenAPI definition file as well as publishing the updated file to an S3 bucket for documentation viewing. 4 | 5 | ---- 6 | 7 | * [Project overview](#project-overview) 8 | * [Solution overview](#solution-overview) 9 | * [Key points of the solution](#key-points-of-the-solution) 10 | * [Deploying the solution](#deploying-the-solution) 11 | * [Testing the Amazon API Gateway endpoints](#testing-the-amazon-api-gateway-endpoints) 12 | * [Ping endpoint](#ping-endpoint) 13 | * [Greeting endpoint](#greeting-endpoint) 14 | * [Viewing the API documentation](#viewing-the-api-documentation) 15 | * [Clean-up the solution](#clean-up-the-solution) 16 | * [Conclusion](#conclusion) 17 | * [Executing unit tests](#executing-unit-tests) 18 | * [Executing static code analysis tool](#executing-static-code-analysis-tool) 19 | * [Security](#security) 20 | * [License](#license) 21 | 22 | ## Project overview 23 | 24 | [Amazon API Gateway](https://aws.amazon.com/api-gateway/) is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale. APIs act as the "front door" for applications to access data, business logic, or functionality from your backend services. Using API Gateway, you can create RESTful APIs and WebSocket APIs that enable real-time two-way communication applications. API Gateway supports containerized and serverless workloads, as well as web applications. 25 | 26 | The [OpenAPI Specification (OAS)](https://swagger.io/specification/) defines a standard, language-agnostic interface to RESTful APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. When properly defined, a consumer can understand and interact with the remote service with a minimal amount of implementation logic. An OpenAPI definition can then be used by documentation generation tools to display the API. 27 | 28 | This blog post will describe how an OpenAPI definition can be used to define, document and create an Amazon API Gateway deployment from a single definition file. At deploy time, a [CloudFormation Custom Resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) is leveraged to dynamically substitute the Lambda integration function ARNs into the OpenAPI definition file as well as publishing the updated file to an S3 bucket for documentation viewing. 29 | 30 | ## Solution overview 31 | 32 | The solution architecture discussed in this post is presented below: 33 | 34 | ![Solution Architecture](docs/assets/solution-architecture.png) 35 | 36 | 1. CDK is used to synthesize a CloudFormation template. 37 | 2. The generated CloudFormation template includes the definition of a custom resource. The custom resource is implemented via a Lambda function which will dynamically substitute the function ARNs of the Amazon API Gateway Lambda integrations into the OpenAPI definition file. 38 | 3. Once the custom resource has completed the substitution processes, the resulting OpenAPI definition file is used to create the Amazon API Gateway. The same OpenAPI definition file is then published to an S3 bucket to allow for documentation viewing. 39 | 4. Upon successful deployment of the CloudFormation stack, end users can invoke the Amazon API Gateway. They can also view the API documentation via the [Swagger UI](https://swagger.io/tools/swagger-ui/). 40 | 41 | ## Key points of the solution 42 | 43 | The relevant section of the CDK [stacks/apigateway_dynamic_publish.py](stacks/apigateway_dynamic_publish.py) stack, in which the Custom Resource and Lambda are defined, is shown below: 44 | 45 | ```python 46 | # Create a role for the api creator lambda function 47 | apicreator_lambda_role = iam.Role( 48 | scope=self, 49 | id="ApiCreatorLambdaRole", 50 | assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), 51 | managed_policies=[ 52 | iam.ManagedPolicy.from_aws_managed_policy_name( 53 | "service-role/AWSLambdaBasicExecutionRole" 54 | ) 55 | ] 56 | ) 57 | apicreator_lambda_role.add_to_policy( 58 | iam.PolicyStatement( 59 | effect=iam.Effect.ALLOW, 60 | resources=[ 61 | "arn:aws:apigateway:*::/apis/*", 62 | "arn:aws:apigateway:*::/apis" 63 | ], 64 | actions=[ 65 | "apigateway:DELETE", 66 | "apigateway:PUT", 67 | "apigateway:PATCH", 68 | "apigateway:POST", 69 | "apigateway:GET" 70 | ] 71 | ) 72 | ) 73 | apicreator_lambda_role.add_to_policy( 74 | iam.PolicyStatement( 75 | effect=iam.Effect.ALLOW, 76 | resources=["*"], 77 | actions=[ 78 | "logs:*" 79 | ] 80 | ) 81 | ) 82 | api_documentation_bucket.grant_read_write(apicreator_lambda_role) 83 | apicreator_lambda = aws_lambda.Function( 84 | scope=self, 85 | id="ApiCreatorLambda", 86 | code=aws_lambda.Code.from_asset( 87 | f"{os.path.dirname(__file__)}/resources/api_creation", 88 | bundling=BundlingOptions( 89 | image=aws_lambda.Runtime.PYTHON_3_9.bundling_image, 90 | command=[ 91 | "bash", "-c", 92 | "pip install --no-cache -r requirements.txt -t /asset-output && cp -au . /asset-output" 93 | ], 94 | ), 95 | ), 96 | handler="api_creator.lambda_handler", 97 | role=apicreator_lambda_role, 98 | runtime=aws_lambda.Runtime.PYTHON_3_9, 99 | timeout=Duration.minutes(5) 100 | ) 101 | # Provider that invokes the api creator lambda function 102 | apicreator_provider = custom_resources.Provider( 103 | self, 104 | 'ApiCreatorCustomResourceProvider', 105 | on_event_handler=apicreator_lambda 106 | ) 107 | # The custom resource that uses the api creator provider to supply values 108 | apicreator_custom_resource = CustomResource( 109 | self, 110 | 'ApiCreatorCustomResource', 111 | service_token=apicreator_provider.service_token, 112 | properties={ 113 | 'ApiGatewayAccessLogsLogGroupArn': api_gateway_access_log_group.log_group_arn, 114 | 'ApiIntegrationPingLambda': api_gateway_ping_lambda.function_arn, 115 | 'ApiIntegrationGreetingLambda': api_gateway_greeting_lambda.function_arn, 116 | 'ApiDocumentationBucketName': api_documentation_bucket.bucket_name, 117 | 'ApiDocumentationBucketUrl': api_documentation_bucket.bucket_website_url, 118 | 'ApiName': f"{config['api']['apiName']}", 119 | 'ApiStageName': config['api']['apiStageName'], 120 | 'ThrottlingBurstLimit': config['api']['throttlingBurstLimit'], 121 | 'ThrottlingRateLimit': config['api']['throttlingRateLimit'] 122 | } 123 | )) 124 | 125 | apigateway_id = CustomResource.get_att_string(apicreator_custom_resource, attribute_name='ApiId') 126 | apigateway_endpoint = CustomResource.get_att_string(apicreator_custom_resource, attribute_name='ApiEndpoint') 127 | apigateway_stagename = CustomResource.get_att_string(apicreator_custom_resource, attribute_name='ApiStageName') 128 | ``` 129 | 130 | ----- 131 | 132 | The [stacks/resources/api_creation/api_creator.py](stacks/resources/api_creation/api_creator.py) Lambda function, called by the Custom Resource, is shown below: 133 | 134 | ```python 135 | #!/usr/bin/env python 136 | 137 | """ 138 | api_creator.py: 139 | Cloudformation custom resource lambda handler which performs the following tasks: 140 | * injects lambda functions arns (created during CDK deployment) into the 141 | OpenAPI 3 spec file (api_definition.yaml) 142 | * deploys or updates the API Gateway stage using the OpenAPI 3 spec file (api_definition.yaml) 143 | * deletes the API Gateway stage (if the Cloudformation operation is delete) 144 | """ 145 | 146 | import json 147 | import logging 148 | import os 149 | 150 | import boto3 151 | import yaml 152 | 153 | # set logging 154 | logger = logging.getLogger() 155 | logger.setLevel(logging.DEBUG) 156 | 157 | # environment variables 158 | aws_region = os.environ['AWS_REGION'] 159 | 160 | # boto3 clients 161 | apigateway_client = boto3.client('apigatewayv2') 162 | s3_client = boto3.client('s3') 163 | 164 | def replace_placeholders(template_file: str, substitutions: dict) -> str: 165 | import re 166 | 167 | def from_dict(dct): 168 | def lookup(match): 169 | key = match.group(1) 170 | return dct.get(key, f'<{key} not found>') 171 | return lookup 172 | 173 | with open (template_file, "r") as template_file: 174 | template_data = template_file.read() 175 | 176 | # perform the subsitutions, looking for placeholders @@PLACEHOLDER@@ 177 | api_template = re.sub('@@(.*?)@@', from_dict(substitutions), template_data) 178 | 179 | return api_template 180 | 181 | 182 | def get_api_by_name(api_name: str) -> str: 183 | get_apis = apigateway_client.get_apis() 184 | for api in get_apis['Items']: 185 | if api['Name'] == api_name: 186 | return api['ApiId'] 187 | 188 | return None 189 | 190 | 191 | def create_api(api_template: str) -> str: 192 | api_response = apigateway_client.import_api( 193 | Body=api_template, 194 | FailOnWarnings=True 195 | ) 196 | 197 | return api_response['ApiEndpoint'], api_response['ApiId'] 198 | 199 | 200 | def update_api(api_template: str, api_name: str) -> str: 201 | 202 | api_id = get_api_by_name(api_name) 203 | 204 | if api_id is not None: 205 | api_response = apigateway_client.reimport_api( 206 | ApiId=api_id, 207 | Body=api_template, 208 | FailOnWarnings=True 209 | ) 210 | return api_response['ApiEndpoint'], api_response['ApiId'] 211 | 212 | 213 | def delete_api(api_name: str) -> None: 214 | if get_api_by_name(api_name) is not None: 215 | apigateway_client.delete_api( 216 | ApiId=get_api_by_name(api_name) 217 | ) 218 | 219 | 220 | def deploy_api( 221 | api_id: str, 222 | api_stage_name: str, 223 | api_access_logs_arn: str, 224 | throttling_burst_limit: int, 225 | throttling_rate_limit: int 226 | ) -> None: 227 | apigateway_client.create_stage( 228 | AccessLogSettings={ 229 | 'DestinationArn': api_access_logs_arn, 230 | 'Format': '$context.identity.sourceIp - - [$context.requestTime] "$context.httpMethod $context.routeKey $context.protocol" $context.status $context.responseLength $context.requestId $context.integrationErrorMessage' 231 | }, 232 | ApiId=api_id, 233 | StageName=api_stage_name, 234 | AutoDeploy=True, 235 | DefaultRouteSettings={ 236 | 'DetailedMetricsEnabled': True, 237 | 'ThrottlingBurstLimit':throttling_burst_limit, 238 | 'ThrottlingRateLimit': throttling_rate_limit 239 | } 240 | ) 241 | 242 | 243 | def delete_api_deployment(api_id: str, api_stage_name: str) -> None: 244 | try: 245 | apigateway_client.get_stage( 246 | ApiId=api_id, 247 | StageName=api_stage_name 248 | ) 249 | 250 | apigateway_client.delete_stage( 251 | ApiId=api_id, 252 | StageName=api_stage_name 253 | ) 254 | except apigateway_client.exceptions.NotFoundException as e: 255 | logger.error(f"Stage name: {api_stage_name} for api id: {api_id} was not found during stage deletion. This is an expected error condition and is handled in code.") 256 | except Exception as e: 257 | raise ValueError(f"Unexpected error encountered during api deployment deletion: {str(e)}") 258 | 259 | 260 | def publish_api_documentation(bucket_name: str, api_definition: str) -> None: 261 | 262 | api_definition_json=json.dumps(yaml.safe_load(api_definition)) 263 | 264 | with open("/tmp/swagger.json", "w") as swagger_file: 265 | swagger_file.write(api_definition_json) 266 | 267 | # Upload the file 268 | try: 269 | 270 | s3_client.upload_file("/tmp/swagger.json", bucket_name, "swagger.json") 271 | 272 | except Exception as e: 273 | logging.error(str(e)) 274 | raise ValueError(str(e)) 275 | 276 | 277 | def lambda_handler(event, context): 278 | 279 | # print the event details 280 | logger.debug(json.dumps(event, indent=2)) 281 | 282 | props = event['ResourceProperties'] 283 | api_gateway_access_log_group_arn = props['ApiGatewayAccessLogsLogGroupArn'] 284 | api_integration_ping_lambda = props['ApiIntegrationPingLambda'] 285 | api_integration_greetings_lambda = props['ApiIntegrationGreetingLambda'] 286 | api_name = props['ApiName'] 287 | api_stage_name = props['ApiStageName'] 288 | api_documentation_bucket_name = props['ApiDocumentationBucketName'] 289 | throttling_burst_limit = int(props['ThrottlingBurstLimit']) 290 | throttling_rate_limit = int(props['ThrottlingRateLimit']) 291 | 292 | lambda_substitutions = { 293 | "API_NAME": api_name, 294 | "API_INTEGRATION_PING_LAMBDA": f"arn:aws:apigateway:{aws_region}:lambda:path/2015-03-31/functions/{api_integration_ping_lambda}/invocations", 295 | "API_INTEGRATION_GREETING_LAMBDA": f"arn:aws:apigateway:{aws_region}:lambda:path/2015-03-31/functions/{api_integration_greetings_lambda}/invocations" 296 | } 297 | 298 | api_template = replace_placeholders("api_definition.yaml", lambda_substitutions) 299 | 300 | if event['RequestType'] != 'Delete': 301 | 302 | if get_api_by_name(api_name) is None: 303 | 304 | logger.debug("Creating API") 305 | 306 | api_endpoint, api_id = create_api(api_template) 307 | 308 | deploy_api(api_id, api_stage_name, api_gateway_access_log_group_arn, throttling_burst_limit, throttling_rate_limit) 309 | 310 | publish_api_documentation(api_documentation_bucket_name, api_template) 311 | 312 | output = { 313 | 'PhysicalResourceId': f"generated-api", 314 | 'Data': { 315 | 'ApiEndpoint': api_endpoint, 316 | 'ApiId': api_id, 317 | 'ApiStageName': api_stage_name 318 | } 319 | } 320 | 321 | return output 322 | 323 | else: 324 | 325 | logger.debug("Updating API") 326 | 327 | api_endpoint, api_id = update_api(api_template, api_name) 328 | 329 | # delete and redeploy the stage after updating the api definition 330 | delete_api_deployment(api_id, api_stage_name) 331 | deploy_api(api_id, api_stage_name, api_gateway_access_log_group_arn, throttling_burst_limit, throttling_rate_limit) 332 | 333 | publish_api_documentation(api_documentation_bucket_name, api_template) 334 | 335 | output = { 336 | 'PhysicalResourceId': f"generated-api", 337 | 'Data': { 338 | 'ApiEndpoint': api_endpoint, 339 | 'ApiId': api_id, 340 | 'ApiStageName': api_stage_name 341 | } 342 | } 343 | 344 | return output 345 | 346 | if event['RequestType'] == 'Delete': 347 | 348 | logger.debug("Deleting API") 349 | 350 | if get_api_by_name(api_name) is not None: 351 | delete_api(api_name) 352 | 353 | output = { 354 | 'PhysicalResourceId': f"generated-api", 355 | 'Data': { 356 | 'ApiEndpoint': "Deleted", 357 | 'ApiId': "Deleted", 358 | 'ApiStageName': "Deleted" 359 | } 360 | } 361 | logger.info(output) 362 | 363 | return output 364 | ``` 365 | 366 | ----- 367 | 368 | The OpenAPI definition file, [stacks/resources/api_creation/api_definition.yaml](stacks/resources/api_creation/api_definition.yaml), is shown below. Note the presence of the dynamic variables represented as `@@VARIABLE_NAME@@` which will be replaced by the custom resource. 369 | 370 | ```yaml 371 | openapi: "3.0.0" 372 | info: 373 | title: @@API_NAME@@ 374 | version: "v1.0" 375 | x-amazon-apigateway-request-validators: 376 | all: 377 | validateRequestBody: true 378 | validateRequestParameters: true 379 | params-only: 380 | validateRequestBody: false 381 | validateRequestParameters: true 382 | x-amazon-apigateway-request-validator: all 383 | paths: 384 | /ping: 385 | get: 386 | summary: "Simulates an API Ping" 387 | description: | 388 | ## Simulates an API Ping 389 | 390 | The purpose of this endpoint is to simulate a Ping request and respond with a Pong answer. 391 | operationId: "pingIntegration" 392 | x-amazon-apigateway-request-validator: all 393 | responses: 394 | 200: 395 | description: "OK" 396 | content: 397 | application/json: 398 | schema: 399 | type: array 400 | items: 401 | $ref: "#/components/schemas/PingResponse" 402 | 500: 403 | description: "Internal Server Error" 404 | x-amazon-apigateway-integration: 405 | uri: @@API_INTEGRATION_PING_LAMBDA@@ 406 | payloadFormatVersion: "2.0" 407 | httpMethod: "POST" 408 | type: "aws_proxy" 409 | connectionType: "INTERNET" 410 | /greeting: 411 | get: 412 | summary: "Get a greeting message" 413 | description: | 414 | ## Get a greeting message 415 | 416 | The purpose of this endpoint is send a greeting string and receive a greeting message. 417 | operationId: "greetingIntegration" 418 | x-amazon-apigateway-request-validator: all 419 | parameters: 420 | - in: query 421 | name: greeting 422 | schema: 423 | type: string 424 | description: | 425 | A greeting string which the API will combine to form a greeting message 426 | responses: 427 | 200: 428 | description: "OK" 429 | content: 430 | application/json: 431 | schema: 432 | type: array 433 | items: 434 | $ref: "#/components/schemas/GreetingResponse" 435 | 500: 436 | description: "Internal Server Error" 437 | x-amazon-apigateway-integration: 438 | uri: @@API_INTEGRATION_GREETING_LAMBDA@@ 439 | payloadFormatVersion: "2.0" 440 | httpMethod: "POST" 441 | type: "aws_proxy" 442 | connectionType: "INTERNET" 443 | components: 444 | schemas: 445 | PingResponse: 446 | type: object 447 | properties: 448 | ping: 449 | type: string 450 | description: | 451 | Response to the ping request. 452 | GreetingResponse: 453 | type: object 454 | properties: 455 | greeting: 456 | type: string 457 | description: | 458 | The greeting response which concatenates the incoming greeting to form a greeting message. 459 | ``` 460 | 461 | ## Deploying the solution 462 | 463 | The solution code uses the Python flavour of the AWS CDK ([Cloud Development Kit](https://aws.amazon.com/cdk/)). In order to execute the solution code, please ensure that you have fulfilled the [AWS CDK Prerequisites for Python](https://docs.aws.amazon.com/cdk/latest/guide/work-with-cdk-python.html). 464 | 465 | Additionally, the project assumes: 466 | 467 | * configuration of [AWS CLI Environment Variables](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html). 468 | * the availability of a `bash` (or compatible) shell environment. 469 | * a [Docker](https://www.docker.com/) installation. 470 | 471 | The solution code requires that the AWS account is [bootstrapped](https://docs.aws.amazon.com/de_de/cdk/latest/guide/bootstrapping.html) in order to allow the deployment of the solution’s CDK stack. 472 | 473 | ```bash 474 | # navigate to project directory 475 | cd api-gateway-dynamic-publish 476 | 477 | # install and activate a Python Virtual Environment 478 | python3 -m venv .venv 479 | source .venv/bin/activate 480 | 481 | # install dependant libraries 482 | python -m pip install -r requirements.txt 483 | 484 | # bootstrap the account to permit CDK deployments 485 | cdk bootstrap 486 | ``` 487 | 488 | Upon successful completion of `cdk bootstrap`, the solution is ready to be deployed. 489 | 490 | The CDK stack can be deployed with the command below. 491 | 492 | ```bash 493 | cdk deploy 494 | ``` 495 | 496 | ![CDK Deploy Screenshot](docs/assets/cdk-deploy-screenshot.png) 497 | 498 | Following a successful deployment, verify that two new stacks have been created. 499 | 500 | * `CDKToolkit` 501 | * `ApiGatewayDynamicPublish` 502 | 503 | Log into the AWS Console → navigate to the CloudFormation console: 504 | [Image: cdk-stacks-screenshot.png]Verify the successful deployment of the Amazon API Gateway. 505 | 506 | 1. Log into the AWS Console → navigate to the API Gateway console. 507 | 2. Click on the API with name `apigateway-dynamic-publish` to open the detailed API view. 508 | 3. Click on `Develop` → `Integrations` and verify the `/greeting` and `/ping` routes. 509 | 510 | ![API Gateway Routes](docs/assets/api-gateway-routes.png) 511 | 512 | The CDK stack has successfully deployed the Amazon API Gateway according to the specifications described in the OpenAPI definition file, [stacks/resources/api_creation/api_definition.yaml](stacks/resources/api_creation/api_definition.yaml). 513 | 514 | ## Testing the Amazon API Gateway endpoints 515 | 516 | The project includes 2 test scripts that can be executed to test the `/ping` and `/greeting` API endpoints respectively. 517 | 518 | ### Ping endpoint 519 | 520 | Test the `/ping` API endpoint with the command below: 521 | 522 | ```bash 523 | bash examples/test_ping_api.sh 524 | ``` 525 | 526 | An example of a successful response is shown below: 527 | 528 | ```bash 529 | Testing GET https://xxxxxxxx.execute-api.xx-xxxx-x.amazonaws.com/dev/ping 530 | 531 | { "ping": "Pong" } 532 | ``` 533 | 534 | ### Greeting endpoint 535 | 536 | Test the `/greeting` API endpoint with the command below: 537 | 538 | ```bash 539 | bash examples/test_greeting_api.sh 540 | ``` 541 | 542 | An example of a successful response is shown below: 543 | 544 | ```bash 545 | Testing GET https://xxxxxxxx.execute-api.xx-xxxx-x.amazonaws.com/dev/greeting?greeting=world 546 | 547 | { "greeting": "Hello world" } 548 | ``` 549 | 550 | ## Viewing the API documentation 551 | 552 | During the project deployment, the OpenAPI definition file [stacks/resources/api_creation/api_definition.yaml](stacks/resources/api_creation/api_definition.yaml), is uploaded to an S3 bucket where it can be consumed to visualize the API documentation via [Swagger UI](https://github.com/swagger-api/swagger-ui). 553 | 554 | To view API documentation in the OpenAPI format, it is necessary to download Swagger UI which is a third party, open source project licensed under the Apache License 2.0. 555 | 556 | More information about Swagger UI can be found on the project's GitHub page; https://github.com/swagger-api/swagger-ui. 557 | 558 | The command below can be used to launch the API documentation viewer. The command will ask you for permission to download Swagger UI. 559 | 560 | ```bash 561 | bash apidocs/view_api_docs.sh 562 | ``` 563 | 564 | The command starts a [Node.js](https://nodejs.org/en/) server listening at [http://localhost:12345](http://localhost:12345/) 565 | 566 | Open [http://localhost:12345](http://localhost:12345/) in your preferred browser to view the API documentation. 567 | 568 | ![Swagger UI Screenshot](docs/assets/swagger-ui-screenshot.png) 569 | 570 | ## Clean-up the solution 571 | 572 | Solution clean-up is a 2 step process: 573 | 574 | 1. Destroy the CDK stack. 575 | 2. Delete the *CDKToolkit* stack from CloudFormation. 576 | 577 | Delete the stack deployed by CDK with the command below: 578 | 579 | ```bash 580 | cdk destroy 581 | ``` 582 | 583 | ![CDK Destroy Screenshot](docs/assets/cdk-destroy-screenshot.png) 584 | 585 | Delete the CDKToolkit CloudFormation stack. 586 | 587 | 1. Log into the AWS Console → navigate to the *CloudFormation* console. 588 | 2. Navigate to *Stacks*. 589 | 3. Select the **CDKToolkit**. 590 | 4. Click the *Delete* button. 591 | 592 | ## Conclusion 593 | 594 | In this blog post we have a seen how an OpenAPI definition file can be leveraged to define, document and create an Amazon API Gateway. 595 | 596 | A challenge that arises when defining, documenting and creating an Amazon API Gateway via an Infrastructure as Code approach is that the function ARNs of the Amazon API Gateway Lambda integrations must be added to the OpenAPI definition file prior to API Gateway creation. The function ARNs are not generated until deploy time. 597 | 598 | This solution provides a [CloudFormation Custom Resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) which overcomes this challenge by dynamically substituting the Lambda integration function ARNs into the OpenAPI definition file allowing for the creation of the Amazon API Gateway and the publishing of the API documentation to an S3 bucket. 599 | 600 | This blog post focuses on an Amazon API Gateway specific use case, however the Custom Resource pattern is very flexible and can be used to extend and enhance the functionality of CloudFormation templates and CDK stacks across a variety of use case scenarios. 601 | 602 | # Executing unit tests 603 | 604 | Unit tests for the project can be executed via the command below: 605 | 606 | ```bash 607 | python3 -m venv .venv 608 | source .venv/bin/activate 609 | cdk synth && python -m pytest 610 | ``` 611 | 612 | # Executing static code analysis tool 613 | 614 | The solution includes [Checkov](https://github.com/bridgecrewio/checkov) which is a static code analysis tool for infrastructure as code (IaC). 615 | 616 | The static code analysis tool for the project can be executed via the commands below: 617 | 618 | ```bash 619 | python3 -m venv .venv 620 | source .venv/bin/activate 621 | rm -fr cdk.out && cdk synth && checkov --config-file checkov.yaml 622 | ``` 623 | 624 | **NOTE:** The Checkov tool has been configured to skip certain checks. 625 | 626 | The Checkov configuration file, [checkov.yaml](checkov.yaml), contains a section named `skip-check`. 627 | 628 | ``` 629 | skip-check: 630 | - CKV_AWS_7 # Ensure rotation for customer created CMKs is enabled 631 | - CKV_AWS_18 # Ensure the S3 bucket has access logging enabled 632 | - CKV_AWS_19 # Ensure the S3 bucket has server-side-encryption enabled 633 | - CKV_AWS_20 # Ensure the S3 bucket does not allow READ permissions to everyone 634 | - CKV_AWS_21 # Ensure the S3 bucket has versioning enabled 635 | - CKV_AWS_33 # Ensure KMS key policy does not contain wildcard (*) principal 636 | - CKV_AWS_40 # Ensure IAM policies are attached only to groups or roles (Reducing access management complexity may in-turn reduce opportunity for a principal to inadvertently receive or retain excessive privileges.) 637 | - CKV_AWS_45 # Ensure no hard-coded secrets exist in lambda environment 638 | - CKV_AWS_53 # Ensure S3 bucket has block public ACLS enabled 639 | - CKV_AWS_54 # Ensure S3 bucket has block public policy enabled 640 | - CKV_AWS_55 # Ensure S3 bucket has ignore public ACLs enabled 641 | - CKV_AWS_56 # Ensure S3 bucket has 'restrict_public_bucket' enabled 642 | - CKV_AWS_57 # Ensure the S3 bucket does not allow WRITE permissions to everyone 643 | - CKV_AWS_60 # Ensure IAM role allows only specific services or principals to assume it 644 | - CKV_AWS_61 # Ensure IAM role allows only specific principals in account to assume it 645 | - CKV_AWS_62 # Ensure no IAM policies that allow full "*-*" administrative privileges are not created 646 | - CKV_AWS_63 # Ensure no IAM policies documents allow "*" as a statement's actions 647 | - CKV_AWS_66 # Ensure that CloudWatch Log Group specifies retention days 648 | - CKV_AWS_107 # Ensure IAM policies does not allow credentials exposure 649 | - CKV_AWS_108 # Ensure IAM policies does not allow data exfiltration 650 | - CKV_AWS_109 # Ensure IAM policies does not allow permissions management without constraints 651 | - CKV_AWS_110 # Ensure IAM policies does not allow privilege escalation 652 | - CKV_AWS_111 # Ensure IAM policies does not allow write access without constraints 653 | - CKV_AWS_115 # Ensure that AWS Lambda function is configured for function-level concurrent execution limit 654 | - CKV_AWS_116 # Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ) 655 | - CKV_AWS_117 # Ensure that AWS Lambda function is configured inside a VPC 656 | - CKV_AWS_119 # Ensure DynamoDB Tables are encrypted using a KMS Customer Managed CMK 657 | - CKV_AWS_158 # Ensure that CloudWatch Log Group is encrypted by KMS 658 | - CKV_AWS_173 # Check encryption settings for Lambda environmental variable 659 | ``` 660 | 661 | These checks represent best practices in AWS and should be enabled (or at the very least the security risk of not enabling the checks should be accepted and understood) for production systems. 662 | 663 | In the context of this solution, these specific checks have not been remediated in order to focus on the core elements of the solution. 664 | 665 | # Security 666 | 667 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 668 | 669 | # License 670 | 671 | This library is licensed under the MIT-0 License. See the LICENSE file. -------------------------------------------------------------------------------- /apidocs/server.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | 3 | var app = express(); 4 | 5 | app.use(express.static('swagger-ui')); 6 | 7 | //make way for some custom css, js and images 8 | app.use('/', express.static(__dirname + '/swagger-ui')); 9 | 10 | var server = app.listen(12345, function(){ 11 | var port = server.address().port; 12 | console.log("Server started at http://localhost:%s", port); 13 | }); -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import aws_cdk 4 | 5 | from stacks.apigateway_dynamic_publish import ApiGatewayDynamicPublishStack 6 | 7 | app = aws_cdk.App() 8 | ApiGatewayDynamicPublishStack(app, "ApiGatewayDynamicPublish") 9 | 10 | app.synth() 11 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py", 3 | "context": { 4 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 5 | "aws-cdk:enableDiffNoFail": "true", 6 | "@aws-cdk/core:stackRelativeExports": "true", 7 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, 8 | "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, 9 | "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true, 10 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 11 | "@aws-cdk/aws-efs:defaultEncryptionAtRest": true, 12 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 13 | "api": { 14 | "apiName": "apigateway-dynamic-publish", 15 | "apiStageName": "dev", 16 | "throttlingBurstLimit": 500, 17 | "throttlingRateLimit":100 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /checkov.yaml: -------------------------------------------------------------------------------- 1 | branch: main 2 | download-external-modules: true 3 | evaluate-variables: true 4 | external-modules-download-path: .external_modules 5 | framework: cloudformation 6 | output: cli 7 | directory: 8 | - cdk.out 9 | skip-check: 10 | - CKV_AWS_7 # Ensure rotation for customer created CMKs is enabled 11 | - CKV_AWS_18 # Ensure the S3 bucket has access logging enabled 12 | - CKV_AWS_19 # Ensure the S3 bucket has server-side-encryption enabled 13 | - CKV_AWS_20 # Ensure the S3 bucket does not allow READ permissions to everyone 14 | - CKV_AWS_21 # Ensure the S3 bucket has versioning enabled 15 | - CKV_AWS_33 # Ensure KMS key policy does not contain wildcard (*) principal 16 | - CKV_AWS_40 # Ensure IAM policies are attached only to groups or roles (Reducing access management complexity may in-turn reduce opportunity for a principal to inadvertently receive or retain excessive privileges.) 17 | - CKV_AWS_45 # Ensure no hard-coded secrets exist in lambda environment 18 | - CKV_AWS_53 # Ensure S3 bucket has block public ACLS enabled 19 | - CKV_AWS_54 # Ensure S3 bucket has block public policy enabled 20 | - CKV_AWS_55 # Ensure S3 bucket has ignore public ACLs enabled 21 | - CKV_AWS_56 # Ensure S3 bucket has 'restrict_public_bucket' enabled 22 | - CKV_AWS_57 # Ensure the S3 bucket does not allow WRITE permissions to everyone 23 | - CKV_AWS_60 # Ensure IAM role allows only specific services or principals to assume it 24 | - CKV_AWS_61 # Ensure IAM role allows only specific principals in account to assume it 25 | - CKV_AWS_62 # Ensure no IAM policies that allow full "*-*" administrative privileges are not created 26 | - CKV_AWS_63 # Ensure no IAM policies documents allow "*" as a statement's actions 27 | - CKV_AWS_66 # Ensure that CloudWatch Log Group specifies retention days 28 | - CKV_AWS_107 # Ensure IAM policies does not allow credentials exposure 29 | - CKV_AWS_108 # Ensure IAM policies does not allow data exfiltration 30 | - CKV_AWS_109 # Ensure IAM policies does not allow permissions management without constraints 31 | - CKV_AWS_110 # Ensure IAM policies does not allow privilege escalation 32 | - CKV_AWS_111 # Ensure IAM policies does not allow write access without constraints 33 | - CKV_AWS_115 # Ensure that AWS Lambda function is configured for function-level concurrent execution limit 34 | - CKV_AWS_116 # Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ) 35 | - CKV_AWS_117 # Ensure that AWS Lambda function is configured inside a VPC 36 | - CKV_AWS_119 # Ensure DynamoDB Tables are encrypted using a KMS Customer Managed CMK 37 | - CKV_AWS_158 # Ensure that CloudWatch Log Group is encrypted by KMS 38 | - CKV_AWS_173 # Check encryption settings for Lambda environmental variable -------------------------------------------------------------------------------- /docs/assets/api-gateway-routes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/docs/assets/api-gateway-routes.png -------------------------------------------------------------------------------- /docs/assets/cdk-deploy-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/docs/assets/cdk-deploy-screenshot.png -------------------------------------------------------------------------------- /docs/assets/cdk-destroy-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/docs/assets/cdk-destroy-screenshot.png -------------------------------------------------------------------------------- /docs/assets/cdk-stacks-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/docs/assets/cdk-stacks-screenshot.png -------------------------------------------------------------------------------- /docs/assets/solution-architecture.drawio: -------------------------------------------------------------------------------- 1 | 7VtbU9s4FP41mek+kLEs3/JIEmDZZadMGbbsEyNixfHiWKntkIRfv5IlO9YlIS1xMV1aBqxjW5dz9J3zSUfuwdF8fZGhxewvEuKkZ1vhugfHPdseOB79zQQbLgB+4HBJlMWhkG0FN/EzFkJLSJdxiHPpwYKQpIgXsnBC0hRPCkmGsoys5MemJJFbXaAIa4KbCUp06dc4LGZcGtj+Vv47jqNZ1TLwBvzOHFUPi5HkMxSSVUMEz3pwlBFS8Kv5eoQTprxKL/y98x13645lOC0OecF7zu5uH2J4+fnLN/w4fL68SJ0TUcsTSpZiwKdfb6hglJBlKPpdbCplLEicFqVC3SH9oe2NrJ5L74xYqW+7ikAt+7IA6CVWhyxQy74sAGr1QGkfqB1sCLSSVL2ltG81Okh/4JAsiyRO8aieehYVRhkKY2qSEUlIRmUpSan2hrNintASoJerWVzgmwWaMK2uKGyobErSQkx+YFdloXhWK508C3Y9X0cMZ320yp1+lJHlomzykk5/4917enk/Yca8R0nBKioy8oirzvVsSP+fswkznMZJonT6CWdFTLFwmsQRq78grDkkSgmeljXSkcRpdFWWxtASvTc1EaJ8hkMxJH3+VpORtorXDZGYzxeYzHGRbegj4i4U0KqciyfKqwZSLSGbNVDqVi8i4R2iuuotgOiFwND34MnSQIND6lBEkWTFjEQkRcnZVjqkhkrDWi/bZ64I03c5Z/7FRbEREwQtCyLPqJ2qzMkym4h+/AGof03uHu/As/sY5bcna//qxBYuFWURLvaMC/Ln2Fj2GibDCSriJ9l5mrQsXr1mHmVr0NqCwqJuoBiKD0i8pdiq7sYrzDfQ/OEkfGSK3KR0RplMe4UeaMiTzFEBZELNgTMDjuZxGHLL4zx+Rg81zGUHOzaadu+807BTR0bRihR8TJg6sfoAejKueOlHbVs9QqbTHLdjNmA0TRdQtw9NL6LOOTbqXuXbKiLTAMcYLxKyKTWPJo9dxseu2PIj+IC+578OElUPHPmN9hACNdOVDO+cZJSnxpQ8qKYzchspfldGKw18TfK4rAeOH0hRkPmL7KE2fmNiGFgO7JfsZVp3VCYqlF347mBUsosdzOsIPMMOXDksQdh3Narhe5WwyTUGsC2qYb8vp+cc6PTcbjk9fYXUs71EEN9U0r/3bckWc8MJn4CnrJPRwydIR8TWDvS349eXnv0bu2YatRhfPslLlbOXgL1Y1/ceqGONSrOdKPXaTlDXZjuD7bVr/bbtDL2K2N/RMmewZHoTpqIjT5/IROCfj4nqiA+Lv9Rlj24f0aNbNgwkhFfEuLucxzHMy0N8+K612Q7vqTjb4QhA19PWkeLhtuPDS6tklC/4QKfxmvWDzy6cnT1hPsl2BBmnz/nLUYKFE8hTyXVsPVQEeqDwvbYCBXxfgcI9MFB43QoUlT8ysOPT60v6+wIVeIVY+dPnBU65EPaZs+6ul62I41HWlS6UnSw8Cos+cXy5WhVJLS48nV8TWn63oKWvXhRojclkOafaEFzmPQGsiuOvBpjV97yBArDO79y4mmWv0PwhRJrFjKkHY/rBlIIwpiH0VIT0WJkcMLSgCk0yXxcC/bEqn6ALTTJT8kR9GxjeBsrbu1MXh1JDeu/cD84sp3FvHGe0Is7wUubTdO44ttwR8E05iGn571ACyXmiCs2aVh6VQBrJYrWC4lkX5gtM+ZeET2Q4TJT+1x1rgWzCQE+BGMlm0JavDt4iIlIFZpu7ZuEfVhlFgiiO16JyXto0S9c4i+ngmUn2J6ZeDK/egeHV7hh1HbwvFnOwmrtFYzwt1inrgo+Q1+mQBx0nsL8v5O3YLvnukFfzzp2EVEnKv1HMQ4v4PhLTWQ98GY8/w7ChOFprqThxiuCLCFFWTz1Y4B5rK98pZ1AjZtYndN4qZvqaY7jRt2w+vEGnvIFnnULof583sH0fgP+RN8jhHifQCro95fzIm0MbvAkd3qnJn7/xY96y8wYDyU7+Tz7m43bKKi8S3sGB1uvYskJPUV2mT9TxVdt23d2Uq7aljrHr7QBH2ZXrfG4R+O8SH617LVeOLo4r18Dx2ZrXqmhrA09/x3hVb4KH8iZ4d9FV8d2j5JQCH8jocl6HrqoaxbbtgU0/cnqbU0uo9mvYbP/GqGbRkoEN62MkJtq5czOVM9XqywfbxF0tK7DOxRl1/oGDdlRM5ZAxyf1+TI2d95f5to8t7ZHaA87+G5ME6LTQM5zdUmnJ8UKjvhd0Tckwy1xd0oFTLTE96Hmrj0VgpxaBb5kF+ZnHaLqeBQG2HJjBwNPw7Ri+A3Faw7e+pXORYVx8YPwD4x8YPwrGbUOmsy2MGz+d0g8G36xQFDHqZt2aDqUwsm4gdnT8yh6d8Wyl4fO8w7m76ZNDeb22k5JrRtuz+Rb0ZZoF9UPyHtBNZLdlIlszkfi6dfznr2sGAGSguIavIt3jAIUWt98v8/XQ9itwePYf -------------------------------------------------------------------------------- /docs/assets/solution-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/docs/assets/solution-architecture.png -------------------------------------------------------------------------------- /docs/assets/swagger-ui-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/docs/assets/swagger-ui-screenshot.png -------------------------------------------------------------------------------- /examples/test_greeting_api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################### 4 | # Script Name : test_greeting_api.sh 5 | # Description : Test the API Gateway Greeting endpoint 6 | # which was deployed as a CDK Stack. 7 | # Args : 8 | # Author : Damian McDonald 9 | ################################################################### 10 | 11 | ### check if AWS credential variables are correctly set 12 | if [ -z "${AWS_ACCESS_KEY_ID}" ] 13 | then 14 | echo "AWS credential variable AWS_ACCESS_KEY_ID is empty." 15 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment." 16 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html" 17 | fi 18 | 19 | if [ -z "${AWS_SECRET_ACCESS_KEY}" ] 20 | then 21 | echo "AWS credential variable AWS_SECRET_ACCESS_KEY is empty." 22 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment." 23 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html" 24 | fi 25 | 26 | if [ -z "${AWS_DEFAULT_REGION}" ] 27 | then 28 | echo "AWS credential variable AWS_DEFAULT_REGION is empty." 29 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment." 30 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html" 31 | fi 32 | ### check if AWS credential variables are correctly set 33 | 34 | # set the greeting you would like to send, ensure it is a single word with no spaces 35 | GREETING="world" 36 | 37 | STACK_NAME="ApiGatewayDynamicPublish" 38 | 39 | # Get the API Endpoint 40 | API_ENDPOINT_URL_EXPORT_NAME="api-gateway-dynamic-publish-url" 41 | API_ENDPOINT_URL=$(aws cloudformation --region ${AWS_DEFAULT_REGION} describe-stacks --stack-name ${STACK_NAME} --query "Stacks[0].Outputs[?ExportName=='${API_ENDPOINT_URL_EXPORT_NAME}'].OutputValue" --output text) 42 | API_GATEWAY_URL="${API_ENDPOINT_URL}" 43 | 44 | ################################################ 45 | # TEST Greetings API 46 | ################################################ 47 | 48 | echo "Testing GET ${API_GATEWAY_URL}/greeting?greeting=${GREETING}" 49 | API_RESPONSE=$(curl -sX GET ${API_GATEWAY_URL}/greeting?greeting=${GREETING}) 50 | 51 | echo "" 52 | echo ${API_RESPONSE} 53 | echo "" -------------------------------------------------------------------------------- /examples/test_ping_api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################### 4 | # Script Name : test_ping_api.sh 5 | # Description : Test the API Gateway Ping endpoint 6 | # which was deployed as a CDK Stack. 7 | # Args : 8 | # Author : Damian McDonald 9 | ################################################################### 10 | 11 | ### check if AWS credential variables are correctly set 12 | if [ -z "${AWS_ACCESS_KEY_ID}" ] 13 | then 14 | echo "AWS credential variable AWS_ACCESS_KEY_ID is empty." 15 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment." 16 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html" 17 | fi 18 | 19 | if [ -z "${AWS_SECRET_ACCESS_KEY}" ] 20 | then 21 | echo "AWS credential variable AWS_SECRET_ACCESS_KEY is empty." 22 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment." 23 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html" 24 | fi 25 | 26 | if [ -z "${AWS_DEFAULT_REGION}" ] 27 | then 28 | echo "AWS credential variable AWS_DEFAULT_REGION is empty." 29 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment." 30 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html" 31 | fi 32 | ### check if AWS credential variables are correctly set 33 | 34 | STACK_NAME="ApiGatewayDynamicPublish" 35 | 36 | # Get the API Endpoint 37 | API_ENDPOINT_URL_EXPORT_NAME="api-gateway-dynamic-publish-url" 38 | API_ENDPOINT_URL=$(aws cloudformation --region ${AWS_DEFAULT_REGION} describe-stacks --stack-name ${STACK_NAME} --query "Stacks[0].Outputs[?ExportName=='${API_ENDPOINT_URL_EXPORT_NAME}'].OutputValue" --output text) 39 | API_GATEWAY_URL="${API_ENDPOINT_URL}" 40 | 41 | ################################################ 42 | # TEST Ping API 43 | ################################################ 44 | 45 | echo "Testing GET ${API_GATEWAY_URL}/ping" 46 | API_RESPONSE=$(curl -sX GET ${API_GATEWAY_URL}/ping) 47 | 48 | echo "" 49 | echo ${API_RESPONSE} 50 | echo "" -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-gateway-dynamic-publish", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "aws-cdk": "^2.14.0", 9 | "express": "^4.21.0" 10 | } 11 | }, 12 | "node_modules/accepts": { 13 | "version": "1.3.8", 14 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 15 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 16 | "dependencies": { 17 | "mime-types": "~2.1.34", 18 | "negotiator": "0.6.3" 19 | }, 20 | "engines": { 21 | "node": ">= 0.6" 22 | } 23 | }, 24 | "node_modules/array-flatten": { 25 | "version": "1.1.1", 26 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 27 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 28 | }, 29 | "node_modules/aws-cdk": { 30 | "version": "2.14.0", 31 | "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.14.0.tgz", 32 | "integrity": "sha512-JtAojkiGohbUxQjEa9k0P4Dk4IdNDLbd7S9ValpuTjoJN51+8/E34i+jUd7Fsl7bmaS/gxLafJsbJjnlBLg5cg==", 33 | "bin": { 34 | "cdk": "bin/cdk" 35 | }, 36 | "engines": { 37 | "node": ">= 14.15.0" 38 | }, 39 | "optionalDependencies": { 40 | "fsevents": "2.3.2" 41 | } 42 | }, 43 | "node_modules/body-parser": { 44 | "version": "1.20.3", 45 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", 46 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", 47 | "dependencies": { 48 | "bytes": "3.1.2", 49 | "content-type": "~1.0.5", 50 | "debug": "2.6.9", 51 | "depd": "2.0.0", 52 | "destroy": "1.2.0", 53 | "http-errors": "2.0.0", 54 | "iconv-lite": "0.4.24", 55 | "on-finished": "2.4.1", 56 | "qs": "6.13.0", 57 | "raw-body": "2.5.2", 58 | "type-is": "~1.6.18", 59 | "unpipe": "1.0.0" 60 | }, 61 | "engines": { 62 | "node": ">= 0.8", 63 | "npm": "1.2.8000 || >= 1.4.16" 64 | } 65 | }, 66 | "node_modules/bytes": { 67 | "version": "3.1.2", 68 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 69 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 70 | "engines": { 71 | "node": ">= 0.8" 72 | } 73 | }, 74 | "node_modules/call-bind": { 75 | "version": "1.0.7", 76 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 77 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 78 | "dependencies": { 79 | "es-define-property": "^1.0.0", 80 | "es-errors": "^1.3.0", 81 | "function-bind": "^1.1.2", 82 | "get-intrinsic": "^1.2.4", 83 | "set-function-length": "^1.2.1" 84 | }, 85 | "engines": { 86 | "node": ">= 0.4" 87 | }, 88 | "funding": { 89 | "url": "https://github.com/sponsors/ljharb" 90 | } 91 | }, 92 | "node_modules/content-disposition": { 93 | "version": "0.5.4", 94 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 95 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 96 | "dependencies": { 97 | "safe-buffer": "5.2.1" 98 | }, 99 | "engines": { 100 | "node": ">= 0.6" 101 | } 102 | }, 103 | "node_modules/content-type": { 104 | "version": "1.0.5", 105 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 106 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 107 | "engines": { 108 | "node": ">= 0.6" 109 | } 110 | }, 111 | "node_modules/cookie": { 112 | "version": "0.6.0", 113 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", 114 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", 115 | "engines": { 116 | "node": ">= 0.6" 117 | } 118 | }, 119 | "node_modules/cookie-signature": { 120 | "version": "1.0.6", 121 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 122 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 123 | }, 124 | "node_modules/debug": { 125 | "version": "2.6.9", 126 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 127 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 128 | "dependencies": { 129 | "ms": "2.0.0" 130 | } 131 | }, 132 | "node_modules/define-data-property": { 133 | "version": "1.1.4", 134 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 135 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 136 | "dependencies": { 137 | "es-define-property": "^1.0.0", 138 | "es-errors": "^1.3.0", 139 | "gopd": "^1.0.1" 140 | }, 141 | "engines": { 142 | "node": ">= 0.4" 143 | }, 144 | "funding": { 145 | "url": "https://github.com/sponsors/ljharb" 146 | } 147 | }, 148 | "node_modules/depd": { 149 | "version": "2.0.0", 150 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 151 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 152 | "engines": { 153 | "node": ">= 0.8" 154 | } 155 | }, 156 | "node_modules/destroy": { 157 | "version": "1.2.0", 158 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 159 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 160 | "engines": { 161 | "node": ">= 0.8", 162 | "npm": "1.2.8000 || >= 1.4.16" 163 | } 164 | }, 165 | "node_modules/ee-first": { 166 | "version": "1.1.1", 167 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 168 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 169 | }, 170 | "node_modules/encodeurl": { 171 | "version": "2.0.0", 172 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 173 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 174 | "engines": { 175 | "node": ">= 0.8" 176 | } 177 | }, 178 | "node_modules/es-define-property": { 179 | "version": "1.0.0", 180 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", 181 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", 182 | "dependencies": { 183 | "get-intrinsic": "^1.2.4" 184 | }, 185 | "engines": { 186 | "node": ">= 0.4" 187 | } 188 | }, 189 | "node_modules/es-errors": { 190 | "version": "1.3.0", 191 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 192 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 193 | "engines": { 194 | "node": ">= 0.4" 195 | } 196 | }, 197 | "node_modules/escape-html": { 198 | "version": "1.0.3", 199 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 200 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 201 | }, 202 | "node_modules/etag": { 203 | "version": "1.8.1", 204 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 205 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 206 | "engines": { 207 | "node": ">= 0.6" 208 | } 209 | }, 210 | "node_modules/express": { 211 | "version": "4.21.0", 212 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", 213 | "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", 214 | "dependencies": { 215 | "accepts": "~1.3.8", 216 | "array-flatten": "1.1.1", 217 | "body-parser": "1.20.3", 218 | "content-disposition": "0.5.4", 219 | "content-type": "~1.0.4", 220 | "cookie": "0.6.0", 221 | "cookie-signature": "1.0.6", 222 | "debug": "2.6.9", 223 | "depd": "2.0.0", 224 | "encodeurl": "~2.0.0", 225 | "escape-html": "~1.0.3", 226 | "etag": "~1.8.1", 227 | "finalhandler": "1.3.1", 228 | "fresh": "0.5.2", 229 | "http-errors": "2.0.0", 230 | "merge-descriptors": "1.0.3", 231 | "methods": "~1.1.2", 232 | "on-finished": "2.4.1", 233 | "parseurl": "~1.3.3", 234 | "path-to-regexp": "0.1.10", 235 | "proxy-addr": "~2.0.7", 236 | "qs": "6.13.0", 237 | "range-parser": "~1.2.1", 238 | "safe-buffer": "5.2.1", 239 | "send": "0.19.0", 240 | "serve-static": "1.16.2", 241 | "setprototypeof": "1.2.0", 242 | "statuses": "2.0.1", 243 | "type-is": "~1.6.18", 244 | "utils-merge": "1.0.1", 245 | "vary": "~1.1.2" 246 | }, 247 | "engines": { 248 | "node": ">= 0.10.0" 249 | } 250 | }, 251 | "node_modules/finalhandler": { 252 | "version": "1.3.1", 253 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", 254 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", 255 | "dependencies": { 256 | "debug": "2.6.9", 257 | "encodeurl": "~2.0.0", 258 | "escape-html": "~1.0.3", 259 | "on-finished": "2.4.1", 260 | "parseurl": "~1.3.3", 261 | "statuses": "2.0.1", 262 | "unpipe": "~1.0.0" 263 | }, 264 | "engines": { 265 | "node": ">= 0.8" 266 | } 267 | }, 268 | "node_modules/forwarded": { 269 | "version": "0.2.0", 270 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 271 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 272 | "engines": { 273 | "node": ">= 0.6" 274 | } 275 | }, 276 | "node_modules/fresh": { 277 | "version": "0.5.2", 278 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 279 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 280 | "engines": { 281 | "node": ">= 0.6" 282 | } 283 | }, 284 | "node_modules/fsevents": { 285 | "version": "2.3.2", 286 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 287 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 288 | "hasInstallScript": true, 289 | "optional": true, 290 | "os": [ 291 | "darwin" 292 | ], 293 | "engines": { 294 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 295 | } 296 | }, 297 | "node_modules/function-bind": { 298 | "version": "1.1.2", 299 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 300 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 301 | "funding": { 302 | "url": "https://github.com/sponsors/ljharb" 303 | } 304 | }, 305 | "node_modules/get-intrinsic": { 306 | "version": "1.2.4", 307 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", 308 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", 309 | "dependencies": { 310 | "es-errors": "^1.3.0", 311 | "function-bind": "^1.1.2", 312 | "has-proto": "^1.0.1", 313 | "has-symbols": "^1.0.3", 314 | "hasown": "^2.0.0" 315 | }, 316 | "engines": { 317 | "node": ">= 0.4" 318 | }, 319 | "funding": { 320 | "url": "https://github.com/sponsors/ljharb" 321 | } 322 | }, 323 | "node_modules/gopd": { 324 | "version": "1.0.1", 325 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 326 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 327 | "dependencies": { 328 | "get-intrinsic": "^1.1.3" 329 | }, 330 | "funding": { 331 | "url": "https://github.com/sponsors/ljharb" 332 | } 333 | }, 334 | "node_modules/has-property-descriptors": { 335 | "version": "1.0.2", 336 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 337 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 338 | "dependencies": { 339 | "es-define-property": "^1.0.0" 340 | }, 341 | "funding": { 342 | "url": "https://github.com/sponsors/ljharb" 343 | } 344 | }, 345 | "node_modules/has-proto": { 346 | "version": "1.0.3", 347 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", 348 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", 349 | "engines": { 350 | "node": ">= 0.4" 351 | }, 352 | "funding": { 353 | "url": "https://github.com/sponsors/ljharb" 354 | } 355 | }, 356 | "node_modules/has-symbols": { 357 | "version": "1.0.3", 358 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 359 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 360 | "engines": { 361 | "node": ">= 0.4" 362 | }, 363 | "funding": { 364 | "url": "https://github.com/sponsors/ljharb" 365 | } 366 | }, 367 | "node_modules/hasown": { 368 | "version": "2.0.2", 369 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 370 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 371 | "dependencies": { 372 | "function-bind": "^1.1.2" 373 | }, 374 | "engines": { 375 | "node": ">= 0.4" 376 | } 377 | }, 378 | "node_modules/http-errors": { 379 | "version": "2.0.0", 380 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 381 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 382 | "dependencies": { 383 | "depd": "2.0.0", 384 | "inherits": "2.0.4", 385 | "setprototypeof": "1.2.0", 386 | "statuses": "2.0.1", 387 | "toidentifier": "1.0.1" 388 | }, 389 | "engines": { 390 | "node": ">= 0.8" 391 | } 392 | }, 393 | "node_modules/iconv-lite": { 394 | "version": "0.4.24", 395 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 396 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 397 | "dependencies": { 398 | "safer-buffer": ">= 2.1.2 < 3" 399 | }, 400 | "engines": { 401 | "node": ">=0.10.0" 402 | } 403 | }, 404 | "node_modules/inherits": { 405 | "version": "2.0.4", 406 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 407 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 408 | }, 409 | "node_modules/ipaddr.js": { 410 | "version": "1.9.1", 411 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 412 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 413 | "engines": { 414 | "node": ">= 0.10" 415 | } 416 | }, 417 | "node_modules/media-typer": { 418 | "version": "0.3.0", 419 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 420 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 421 | "engines": { 422 | "node": ">= 0.6" 423 | } 424 | }, 425 | "node_modules/merge-descriptors": { 426 | "version": "1.0.3", 427 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", 428 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", 429 | "funding": { 430 | "url": "https://github.com/sponsors/sindresorhus" 431 | } 432 | }, 433 | "node_modules/methods": { 434 | "version": "1.1.2", 435 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 436 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", 437 | "engines": { 438 | "node": ">= 0.6" 439 | } 440 | }, 441 | "node_modules/mime": { 442 | "version": "1.6.0", 443 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 444 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 445 | "bin": { 446 | "mime": "cli.js" 447 | }, 448 | "engines": { 449 | "node": ">=4" 450 | } 451 | }, 452 | "node_modules/mime-db": { 453 | "version": "1.51.0", 454 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", 455 | "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", 456 | "engines": { 457 | "node": ">= 0.6" 458 | } 459 | }, 460 | "node_modules/mime-types": { 461 | "version": "2.1.34", 462 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", 463 | "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", 464 | "dependencies": { 465 | "mime-db": "1.51.0" 466 | }, 467 | "engines": { 468 | "node": ">= 0.6" 469 | } 470 | }, 471 | "node_modules/ms": { 472 | "version": "2.0.0", 473 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 474 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 475 | }, 476 | "node_modules/negotiator": { 477 | "version": "0.6.3", 478 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 479 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 480 | "engines": { 481 | "node": ">= 0.6" 482 | } 483 | }, 484 | "node_modules/object-inspect": { 485 | "version": "1.13.2", 486 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", 487 | "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", 488 | "engines": { 489 | "node": ">= 0.4" 490 | }, 491 | "funding": { 492 | "url": "https://github.com/sponsors/ljharb" 493 | } 494 | }, 495 | "node_modules/on-finished": { 496 | "version": "2.4.1", 497 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 498 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 499 | "dependencies": { 500 | "ee-first": "1.1.1" 501 | }, 502 | "engines": { 503 | "node": ">= 0.8" 504 | } 505 | }, 506 | "node_modules/parseurl": { 507 | "version": "1.3.3", 508 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 509 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 510 | "engines": { 511 | "node": ">= 0.8" 512 | } 513 | }, 514 | "node_modules/path-to-regexp": { 515 | "version": "0.1.10", 516 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", 517 | "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" 518 | }, 519 | "node_modules/proxy-addr": { 520 | "version": "2.0.7", 521 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 522 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 523 | "dependencies": { 524 | "forwarded": "0.2.0", 525 | "ipaddr.js": "1.9.1" 526 | }, 527 | "engines": { 528 | "node": ">= 0.10" 529 | } 530 | }, 531 | "node_modules/qs": { 532 | "version": "6.13.0", 533 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 534 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 535 | "dependencies": { 536 | "side-channel": "^1.0.6" 537 | }, 538 | "engines": { 539 | "node": ">=0.6" 540 | }, 541 | "funding": { 542 | "url": "https://github.com/sponsors/ljharb" 543 | } 544 | }, 545 | "node_modules/range-parser": { 546 | "version": "1.2.1", 547 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 548 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 549 | "engines": { 550 | "node": ">= 0.6" 551 | } 552 | }, 553 | "node_modules/raw-body": { 554 | "version": "2.5.2", 555 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 556 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 557 | "dependencies": { 558 | "bytes": "3.1.2", 559 | "http-errors": "2.0.0", 560 | "iconv-lite": "0.4.24", 561 | "unpipe": "1.0.0" 562 | }, 563 | "engines": { 564 | "node": ">= 0.8" 565 | } 566 | }, 567 | "node_modules/safe-buffer": { 568 | "version": "5.2.1", 569 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 570 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 571 | "funding": [ 572 | { 573 | "type": "github", 574 | "url": "https://github.com/sponsors/feross" 575 | }, 576 | { 577 | "type": "patreon", 578 | "url": "https://www.patreon.com/feross" 579 | }, 580 | { 581 | "type": "consulting", 582 | "url": "https://feross.org/support" 583 | } 584 | ] 585 | }, 586 | "node_modules/safer-buffer": { 587 | "version": "2.1.2", 588 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 589 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 590 | }, 591 | "node_modules/send": { 592 | "version": "0.19.0", 593 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", 594 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 595 | "dependencies": { 596 | "debug": "2.6.9", 597 | "depd": "2.0.0", 598 | "destroy": "1.2.0", 599 | "encodeurl": "~1.0.2", 600 | "escape-html": "~1.0.3", 601 | "etag": "~1.8.1", 602 | "fresh": "0.5.2", 603 | "http-errors": "2.0.0", 604 | "mime": "1.6.0", 605 | "ms": "2.1.3", 606 | "on-finished": "2.4.1", 607 | "range-parser": "~1.2.1", 608 | "statuses": "2.0.1" 609 | }, 610 | "engines": { 611 | "node": ">= 0.8.0" 612 | } 613 | }, 614 | "node_modules/send/node_modules/encodeurl": { 615 | "version": "1.0.2", 616 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 617 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 618 | "engines": { 619 | "node": ">= 0.8" 620 | } 621 | }, 622 | "node_modules/send/node_modules/ms": { 623 | "version": "2.1.3", 624 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 625 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 626 | }, 627 | "node_modules/serve-static": { 628 | "version": "1.16.2", 629 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", 630 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", 631 | "dependencies": { 632 | "encodeurl": "~2.0.0", 633 | "escape-html": "~1.0.3", 634 | "parseurl": "~1.3.3", 635 | "send": "0.19.0" 636 | }, 637 | "engines": { 638 | "node": ">= 0.8.0" 639 | } 640 | }, 641 | "node_modules/set-function-length": { 642 | "version": "1.2.2", 643 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 644 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 645 | "dependencies": { 646 | "define-data-property": "^1.1.4", 647 | "es-errors": "^1.3.0", 648 | "function-bind": "^1.1.2", 649 | "get-intrinsic": "^1.2.4", 650 | "gopd": "^1.0.1", 651 | "has-property-descriptors": "^1.0.2" 652 | }, 653 | "engines": { 654 | "node": ">= 0.4" 655 | } 656 | }, 657 | "node_modules/setprototypeof": { 658 | "version": "1.2.0", 659 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 660 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 661 | }, 662 | "node_modules/side-channel": { 663 | "version": "1.0.6", 664 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 665 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 666 | "dependencies": { 667 | "call-bind": "^1.0.7", 668 | "es-errors": "^1.3.0", 669 | "get-intrinsic": "^1.2.4", 670 | "object-inspect": "^1.13.1" 671 | }, 672 | "engines": { 673 | "node": ">= 0.4" 674 | }, 675 | "funding": { 676 | "url": "https://github.com/sponsors/ljharb" 677 | } 678 | }, 679 | "node_modules/statuses": { 680 | "version": "2.0.1", 681 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 682 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 683 | "engines": { 684 | "node": ">= 0.8" 685 | } 686 | }, 687 | "node_modules/toidentifier": { 688 | "version": "1.0.1", 689 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 690 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 691 | "engines": { 692 | "node": ">=0.6" 693 | } 694 | }, 695 | "node_modules/type-is": { 696 | "version": "1.6.18", 697 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 698 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 699 | "dependencies": { 700 | "media-typer": "0.3.0", 701 | "mime-types": "~2.1.24" 702 | }, 703 | "engines": { 704 | "node": ">= 0.6" 705 | } 706 | }, 707 | "node_modules/unpipe": { 708 | "version": "1.0.0", 709 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 710 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 711 | "engines": { 712 | "node": ">= 0.8" 713 | } 714 | }, 715 | "node_modules/utils-merge": { 716 | "version": "1.0.1", 717 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 718 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", 719 | "engines": { 720 | "node": ">= 0.4.0" 721 | } 722 | }, 723 | "node_modules/vary": { 724 | "version": "1.1.2", 725 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 726 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", 727 | "engines": { 728 | "node": ">= 0.8" 729 | } 730 | } 731 | }, 732 | "dependencies": { 733 | "accepts": { 734 | "version": "1.3.8", 735 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 736 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 737 | "requires": { 738 | "mime-types": "~2.1.34", 739 | "negotiator": "0.6.3" 740 | } 741 | }, 742 | "array-flatten": { 743 | "version": "1.1.1", 744 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 745 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 746 | }, 747 | "aws-cdk": { 748 | "version": "2.14.0", 749 | "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.14.0.tgz", 750 | "integrity": "sha512-JtAojkiGohbUxQjEa9k0P4Dk4IdNDLbd7S9ValpuTjoJN51+8/E34i+jUd7Fsl7bmaS/gxLafJsbJjnlBLg5cg==", 751 | "requires": { 752 | "fsevents": "2.3.2" 753 | } 754 | }, 755 | "body-parser": { 756 | "version": "1.20.3", 757 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", 758 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", 759 | "requires": { 760 | "bytes": "3.1.2", 761 | "content-type": "~1.0.5", 762 | "debug": "2.6.9", 763 | "depd": "2.0.0", 764 | "destroy": "1.2.0", 765 | "http-errors": "2.0.0", 766 | "iconv-lite": "0.4.24", 767 | "on-finished": "2.4.1", 768 | "qs": "6.13.0", 769 | "raw-body": "2.5.2", 770 | "type-is": "~1.6.18", 771 | "unpipe": "1.0.0" 772 | } 773 | }, 774 | "bytes": { 775 | "version": "3.1.2", 776 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 777 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" 778 | }, 779 | "call-bind": { 780 | "version": "1.0.7", 781 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 782 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 783 | "requires": { 784 | "es-define-property": "^1.0.0", 785 | "es-errors": "^1.3.0", 786 | "function-bind": "^1.1.2", 787 | "get-intrinsic": "^1.2.4", 788 | "set-function-length": "^1.2.1" 789 | } 790 | }, 791 | "content-disposition": { 792 | "version": "0.5.4", 793 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 794 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 795 | "requires": { 796 | "safe-buffer": "5.2.1" 797 | } 798 | }, 799 | "content-type": { 800 | "version": "1.0.5", 801 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 802 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" 803 | }, 804 | "cookie": { 805 | "version": "0.6.0", 806 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", 807 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" 808 | }, 809 | "cookie-signature": { 810 | "version": "1.0.6", 811 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 812 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 813 | }, 814 | "debug": { 815 | "version": "2.6.9", 816 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 817 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 818 | "requires": { 819 | "ms": "2.0.0" 820 | } 821 | }, 822 | "define-data-property": { 823 | "version": "1.1.4", 824 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 825 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 826 | "requires": { 827 | "es-define-property": "^1.0.0", 828 | "es-errors": "^1.3.0", 829 | "gopd": "^1.0.1" 830 | } 831 | }, 832 | "depd": { 833 | "version": "2.0.0", 834 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 835 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 836 | }, 837 | "destroy": { 838 | "version": "1.2.0", 839 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 840 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" 841 | }, 842 | "ee-first": { 843 | "version": "1.1.1", 844 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 845 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 846 | }, 847 | "encodeurl": { 848 | "version": "2.0.0", 849 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 850 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" 851 | }, 852 | "es-define-property": { 853 | "version": "1.0.0", 854 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", 855 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", 856 | "requires": { 857 | "get-intrinsic": "^1.2.4" 858 | } 859 | }, 860 | "es-errors": { 861 | "version": "1.3.0", 862 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 863 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" 864 | }, 865 | "escape-html": { 866 | "version": "1.0.3", 867 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 868 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 869 | }, 870 | "etag": { 871 | "version": "1.8.1", 872 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 873 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" 874 | }, 875 | "express": { 876 | "version": "4.21.0", 877 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", 878 | "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", 879 | "requires": { 880 | "accepts": "~1.3.8", 881 | "array-flatten": "1.1.1", 882 | "body-parser": "1.20.3", 883 | "content-disposition": "0.5.4", 884 | "content-type": "~1.0.4", 885 | "cookie": "0.6.0", 886 | "cookie-signature": "1.0.6", 887 | "debug": "2.6.9", 888 | "depd": "2.0.0", 889 | "encodeurl": "~2.0.0", 890 | "escape-html": "~1.0.3", 891 | "etag": "~1.8.1", 892 | "finalhandler": "1.3.1", 893 | "fresh": "0.5.2", 894 | "http-errors": "2.0.0", 895 | "merge-descriptors": "1.0.3", 896 | "methods": "~1.1.2", 897 | "on-finished": "2.4.1", 898 | "parseurl": "~1.3.3", 899 | "path-to-regexp": "0.1.10", 900 | "proxy-addr": "~2.0.7", 901 | "qs": "6.13.0", 902 | "range-parser": "~1.2.1", 903 | "safe-buffer": "5.2.1", 904 | "send": "0.19.0", 905 | "serve-static": "1.16.2", 906 | "setprototypeof": "1.2.0", 907 | "statuses": "2.0.1", 908 | "type-is": "~1.6.18", 909 | "utils-merge": "1.0.1", 910 | "vary": "~1.1.2" 911 | } 912 | }, 913 | "finalhandler": { 914 | "version": "1.3.1", 915 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", 916 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", 917 | "requires": { 918 | "debug": "2.6.9", 919 | "encodeurl": "~2.0.0", 920 | "escape-html": "~1.0.3", 921 | "on-finished": "2.4.1", 922 | "parseurl": "~1.3.3", 923 | "statuses": "2.0.1", 924 | "unpipe": "~1.0.0" 925 | } 926 | }, 927 | "forwarded": { 928 | "version": "0.2.0", 929 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 930 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" 931 | }, 932 | "fresh": { 933 | "version": "0.5.2", 934 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 935 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" 936 | }, 937 | "fsevents": { 938 | "version": "2.3.2", 939 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 940 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 941 | "optional": true 942 | }, 943 | "function-bind": { 944 | "version": "1.1.2", 945 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 946 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" 947 | }, 948 | "get-intrinsic": { 949 | "version": "1.2.4", 950 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", 951 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", 952 | "requires": { 953 | "es-errors": "^1.3.0", 954 | "function-bind": "^1.1.2", 955 | "has-proto": "^1.0.1", 956 | "has-symbols": "^1.0.3", 957 | "hasown": "^2.0.0" 958 | } 959 | }, 960 | "gopd": { 961 | "version": "1.0.1", 962 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 963 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 964 | "requires": { 965 | "get-intrinsic": "^1.1.3" 966 | } 967 | }, 968 | "has-property-descriptors": { 969 | "version": "1.0.2", 970 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 971 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 972 | "requires": { 973 | "es-define-property": "^1.0.0" 974 | } 975 | }, 976 | "has-proto": { 977 | "version": "1.0.3", 978 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", 979 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" 980 | }, 981 | "has-symbols": { 982 | "version": "1.0.3", 983 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 984 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" 985 | }, 986 | "hasown": { 987 | "version": "2.0.2", 988 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 989 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 990 | "requires": { 991 | "function-bind": "^1.1.2" 992 | } 993 | }, 994 | "http-errors": { 995 | "version": "2.0.0", 996 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 997 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 998 | "requires": { 999 | "depd": "2.0.0", 1000 | "inherits": "2.0.4", 1001 | "setprototypeof": "1.2.0", 1002 | "statuses": "2.0.1", 1003 | "toidentifier": "1.0.1" 1004 | } 1005 | }, 1006 | "iconv-lite": { 1007 | "version": "0.4.24", 1008 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 1009 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 1010 | "requires": { 1011 | "safer-buffer": ">= 2.1.2 < 3" 1012 | } 1013 | }, 1014 | "inherits": { 1015 | "version": "2.0.4", 1016 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1017 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 1018 | }, 1019 | "ipaddr.js": { 1020 | "version": "1.9.1", 1021 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 1022 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 1023 | }, 1024 | "media-typer": { 1025 | "version": "0.3.0", 1026 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 1027 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" 1028 | }, 1029 | "merge-descriptors": { 1030 | "version": "1.0.3", 1031 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", 1032 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" 1033 | }, 1034 | "methods": { 1035 | "version": "1.1.2", 1036 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 1037 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 1038 | }, 1039 | "mime": { 1040 | "version": "1.6.0", 1041 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 1042 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 1043 | }, 1044 | "mime-db": { 1045 | "version": "1.51.0", 1046 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", 1047 | "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" 1048 | }, 1049 | "mime-types": { 1050 | "version": "2.1.34", 1051 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", 1052 | "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", 1053 | "requires": { 1054 | "mime-db": "1.51.0" 1055 | } 1056 | }, 1057 | "ms": { 1058 | "version": "2.0.0", 1059 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1060 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 1061 | }, 1062 | "negotiator": { 1063 | "version": "0.6.3", 1064 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 1065 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" 1066 | }, 1067 | "object-inspect": { 1068 | "version": "1.13.2", 1069 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", 1070 | "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" 1071 | }, 1072 | "on-finished": { 1073 | "version": "2.4.1", 1074 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 1075 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 1076 | "requires": { 1077 | "ee-first": "1.1.1" 1078 | } 1079 | }, 1080 | "parseurl": { 1081 | "version": "1.3.3", 1082 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1083 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 1084 | }, 1085 | "path-to-regexp": { 1086 | "version": "0.1.10", 1087 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", 1088 | "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" 1089 | }, 1090 | "proxy-addr": { 1091 | "version": "2.0.7", 1092 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 1093 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 1094 | "requires": { 1095 | "forwarded": "0.2.0", 1096 | "ipaddr.js": "1.9.1" 1097 | } 1098 | }, 1099 | "qs": { 1100 | "version": "6.13.0", 1101 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 1102 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 1103 | "requires": { 1104 | "side-channel": "^1.0.6" 1105 | } 1106 | }, 1107 | "range-parser": { 1108 | "version": "1.2.1", 1109 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1110 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 1111 | }, 1112 | "raw-body": { 1113 | "version": "2.5.2", 1114 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 1115 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 1116 | "requires": { 1117 | "bytes": "3.1.2", 1118 | "http-errors": "2.0.0", 1119 | "iconv-lite": "0.4.24", 1120 | "unpipe": "1.0.0" 1121 | } 1122 | }, 1123 | "safe-buffer": { 1124 | "version": "5.2.1", 1125 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1126 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 1127 | }, 1128 | "safer-buffer": { 1129 | "version": "2.1.2", 1130 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1131 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1132 | }, 1133 | "send": { 1134 | "version": "0.19.0", 1135 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", 1136 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 1137 | "requires": { 1138 | "debug": "2.6.9", 1139 | "depd": "2.0.0", 1140 | "destroy": "1.2.0", 1141 | "encodeurl": "~1.0.2", 1142 | "escape-html": "~1.0.3", 1143 | "etag": "~1.8.1", 1144 | "fresh": "0.5.2", 1145 | "http-errors": "2.0.0", 1146 | "mime": "1.6.0", 1147 | "ms": "2.1.3", 1148 | "on-finished": "2.4.1", 1149 | "range-parser": "~1.2.1", 1150 | "statuses": "2.0.1" 1151 | }, 1152 | "dependencies": { 1153 | "encodeurl": { 1154 | "version": "1.0.2", 1155 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 1156 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" 1157 | }, 1158 | "ms": { 1159 | "version": "2.1.3", 1160 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1161 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1162 | } 1163 | } 1164 | }, 1165 | "serve-static": { 1166 | "version": "1.16.2", 1167 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", 1168 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", 1169 | "requires": { 1170 | "encodeurl": "~2.0.0", 1171 | "escape-html": "~1.0.3", 1172 | "parseurl": "~1.3.3", 1173 | "send": "0.19.0" 1174 | } 1175 | }, 1176 | "set-function-length": { 1177 | "version": "1.2.2", 1178 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 1179 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 1180 | "requires": { 1181 | "define-data-property": "^1.1.4", 1182 | "es-errors": "^1.3.0", 1183 | "function-bind": "^1.1.2", 1184 | "get-intrinsic": "^1.2.4", 1185 | "gopd": "^1.0.1", 1186 | "has-property-descriptors": "^1.0.2" 1187 | } 1188 | }, 1189 | "setprototypeof": { 1190 | "version": "1.2.0", 1191 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1192 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1193 | }, 1194 | "side-channel": { 1195 | "version": "1.0.6", 1196 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 1197 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 1198 | "requires": { 1199 | "call-bind": "^1.0.7", 1200 | "es-errors": "^1.3.0", 1201 | "get-intrinsic": "^1.2.4", 1202 | "object-inspect": "^1.13.1" 1203 | } 1204 | }, 1205 | "statuses": { 1206 | "version": "2.0.1", 1207 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1208 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" 1209 | }, 1210 | "toidentifier": { 1211 | "version": "1.0.1", 1212 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1213 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" 1214 | }, 1215 | "type-is": { 1216 | "version": "1.6.18", 1217 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1218 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1219 | "requires": { 1220 | "media-typer": "0.3.0", 1221 | "mime-types": "~2.1.24" 1222 | } 1223 | }, 1224 | "unpipe": { 1225 | "version": "1.0.0", 1226 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1227 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" 1228 | }, 1229 | "utils-merge": { 1230 | "version": "1.0.1", 1231 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1232 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1233 | }, 1234 | "vary": { 1235 | "version": "1.1.2", 1236 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1237 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1238 | } 1239 | } 1240 | } 1241 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "aws-cdk": "^2.14.0", 4 | "express": "^4.21.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiodns==3.0.0 2 | aiohttp==3.10.2 3 | aiomultiprocess==0.9.0 4 | aiosignal==1.2.0 5 | argcomplete==2.0.0 6 | async-timeout==4.0.2 7 | attrs==21.4.0 8 | aws-cdk-lib==2.14.0 9 | bc-python-hcl2==0.3.39 10 | beautifulsoup4==4.11.1 11 | boto3==1.21.8 12 | botocore==1.24.8 13 | cached-property==1.5.2 14 | cachetools==5.0.0 15 | cattrs==1.10.0 16 | certifi==2024.7.4 17 | cffi==1.15.0 18 | charset-normalizer==2.0.12 19 | checkov==2.0.1098 20 | click==8.1.3 21 | click-option-group==0.5.3 22 | cloudsplaining==0.5.0 23 | colorama==0.4.4 24 | ConfigArgParse==1.5.3 25 | constructs==10.0.73 26 | contextlib2==21.6.0 27 | cyclonedx-python-lib==0.12.3 28 | decorator==5.1.1 29 | deep-merge==0.0.4 30 | detect-secrets==1.2.0 31 | docker==5.0.3 32 | dockerfile-parse==1.2.0 33 | dpath==1.5.0 34 | frozenlist==1.3.0 35 | gitdb==4.0.9 36 | GitPython==3.1.41 37 | idna==3.7 38 | importlib-metadata==4.11.3 39 | iniconfig==1.1.1 40 | Jinja2==3.1.4 41 | jmespath==0.10.0 42 | jsii==1.54.0 43 | jsonpath-ng==1.5.3 44 | jsonschema==3.2.0 45 | junit-xml==1.9 46 | lark==1.1.2 47 | Markdown==3.3.6 48 | MarkupSafe==2.1.1 49 | multidict==6.0.2 50 | networkx==2.8 51 | packageurl-python==0.9.9 52 | packaging==21.3 53 | pluggy==1.0.0 54 | ply==3.11 55 | policy-sentry==0.12.3 56 | policyuniverse==1.5.0.20220426 57 | prettytable==3.2.0 58 | publication==0.0.3 59 | py==1.11.0 60 | pycares==4.2.0 61 | pycep-parser==0.3.4 62 | pycparser==2.21 63 | pyparsing==3.0.7 64 | pyrsistent==0.18.1 65 | pytest==7.0.1 66 | python-dateutil==2.8.2 67 | PyYAML==6.0 68 | regex==2022.4.24 69 | requests==2.32.2 70 | s3transfer==0.5.2 71 | schema==0.7.5 72 | semantic-version==2.9.0 73 | six==1.16.0 74 | smmap==5.0.0 75 | soupsieve==2.3.2.post1 76 | tabulate==0.8.9 77 | termcolor==1.1.0 78 | toml==0.10.2 79 | tomli==2.0.1 80 | tqdm==4.66.3 81 | types-setuptools==57.4.14 82 | types-toml==0.10.6 83 | typing_extensions==4.1.1 84 | update-checker==0.18.0 85 | urllib3==1.26.19 86 | wcwidth==0.2.5 87 | websocket-client==1.3.2 88 | yarl==1.7.2 89 | zipp==3.19.1 90 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | 4 | with open("README.md") as fp: 5 | long_description = fp.read() 6 | 7 | 8 | setuptools.setup( 9 | name="api-gateway-dynamic-publish", 10 | version="0.0.1", 11 | 12 | description="A sample CDK Python app", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | 16 | author="author", 17 | 18 | package_dir={"": "api-gateway-dynamic-publish"}, 19 | packages=setuptools.find_packages(where="api-gateway-dynamic-publish"), 20 | 21 | install_requires=[ 22 | "aws-cdk.core==2.14.0", 23 | "aws-cdk.aws_iam==2.14.0", 24 | "aws-cdk.aws_lambda==2.14.0", 25 | "aws-cdk.aws_logs==2.14.0", 26 | "aws-cdk.custom_resources==2.14.0", 27 | "aws-cdk.aws_s3==2.14.0", 28 | ], 29 | 30 | python_requires=">=3.6", 31 | 32 | classifiers=[ 33 | "Development Status :: 4 - Beta", 34 | 35 | "Intended Audience :: Developers", 36 | 37 | "Programming Language :: JavaScript", 38 | "Programming Language :: Python :: 3 :: Only", 39 | "Programming Language :: Python :: 3.6", 40 | "Programming Language :: Python :: 3.7", 41 | "Programming Language :: Python :: 3.8", 42 | 43 | "Topic :: Software Development :: Code Generators", 44 | "Topic :: Utilities", 45 | 46 | "Typing :: Typed", 47 | ], 48 | ) 49 | -------------------------------------------------------------------------------- /source.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem The sole purpose of this script is to make the command 4 | rem 5 | rem source .venv/bin/activate 6 | rem 7 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. 8 | rem On Windows, this command just runs this batch file (the argument is ignored). 9 | rem 10 | rem Now we don't need to document a Windows command for activating a virtualenv. 11 | 12 | echo Executing .venv\Scripts\activate.bat for you 13 | .venv\Scripts\activate.bat 14 | -------------------------------------------------------------------------------- /stacks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/stacks/__init__.py -------------------------------------------------------------------------------- /stacks/apigateway_dynamic_publish.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | apigateway_dynamic_publish.py: 5 | CDK Stack that creates and deploys the infrastructure 6 | required for the api-gateway-dynamic-publish project. 7 | """ 8 | 9 | import json 10 | import os 11 | 12 | from aws_cdk import ( 13 | BundlingOptions, 14 | CfnOutput, 15 | CustomResource, 16 | Duration, 17 | RemovalPolicy, 18 | Stack 19 | ) 20 | from aws_cdk import aws_iam as iam 21 | from aws_cdk import aws_lambda 22 | from aws_cdk import aws_logs as logs 23 | from aws_cdk import aws_s3 as s3 24 | from aws_cdk import custom_resources 25 | from constructs import Construct 26 | 27 | 28 | class ApiGatewayDynamicPublishStack(Stack): 29 | """ 30 | CDK Stack that creates and deploys the infrastructure 31 | required for the api-gateway-dynamic-publish project. 32 | """ 33 | 34 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 35 | super().__init__(scope, construct_id, **kwargs) 36 | 37 | config = self.read_cdk_context_json() 38 | 39 | ########################################################## 40 | # API Gateway Documentation Bucket 41 | ########################################################## 42 | 43 | api_documentation_bucket = s3.Bucket( 44 | self, 45 | 'ApiDocumentationBucket', 46 | encryption=s3.BucketEncryption.KMS, 47 | removal_policy=RemovalPolicy.DESTROY, 48 | auto_delete_objects=True, 49 | block_public_access=s3.BlockPublicAccess.BLOCK_ALL, 50 | public_read_access=False, 51 | versioned=True 52 | ) 53 | 54 | ########################################################## 55 | # API Gateway Documentation Bucket 56 | ########################################################## 57 | 58 | 59 | ########################################################## 60 | # API Gateway Access Logs Group 61 | ########################################################## 62 | 63 | # create log group for API Gateway access logs 64 | api_gateway_access_log_group = logs.LogGroup( 65 | self, 66 | 'ApiGatewayAccessLogGroup', 67 | log_group_name='/aws/vendedlogs/ApiGatewayAccessLogs', 68 | removal_policy=RemovalPolicy.DESTROY, 69 | retention=logs.RetentionDays.TWO_WEEKS 70 | ) 71 | 72 | 73 | ########################################################## 74 | # API Gateway Lambda Integrations 75 | ########################################################## 76 | 77 | # IAM Role for Lambda 78 | api_gateway_integration_lambda_role = iam.Role( 79 | scope=self, 80 | id="ApiGatewayIntegrationLambdaRole", 81 | assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), 82 | managed_policies=[ 83 | iam.ManagedPolicy.from_aws_managed_policy_name( 84 | "service-role/AWSLambdaBasicExecutionRole" 85 | ) 86 | ] 87 | ) 88 | 89 | api_gateway_integration_lambda_role.add_to_policy( 90 | iam.PolicyStatement( 91 | effect=iam.Effect.ALLOW, 92 | resources=["*"], 93 | actions=[ 94 | "logs:CreateLogGroup", 95 | "logs:CreateLogStream", 96 | "logs:PutLogEvents", 97 | "logs:ListLogDeliveries" 98 | ] 99 | ) 100 | ) 101 | 102 | api_gateway_ping_lambda = aws_lambda.Function( 103 | scope=self, 104 | id="ApiGatewayPingLambda", 105 | code=aws_lambda.Code.from_asset(f"{os.path.dirname(__file__)}/resources/api_integrations"), 106 | handler="ping.lambda_handler", 107 | role=api_gateway_integration_lambda_role, 108 | runtime=aws_lambda.Runtime.PYTHON_3_9, 109 | timeout=Duration.seconds(15) 110 | ) 111 | 112 | api_gateway_greeting_lambda = aws_lambda.Function( 113 | scope=self, 114 | id="ApiGatewayGreetingLambda", 115 | code=aws_lambda.Code.from_asset(f"{os.path.dirname(__file__)}/resources/api_integrations"), 116 | handler="greeting.lambda_handler", 117 | role=api_gateway_integration_lambda_role, 118 | runtime=aws_lambda.Runtime.PYTHON_3_9, 119 | timeout=Duration.seconds(15) 120 | ) 121 | 122 | ########################################################## 123 | # Create API Creator Custom Resource 124 | ########################################################## 125 | 126 | 127 | ########################################################## 128 | # Create API Creator Custom Resource 129 | ########################################################## 130 | 131 | # Create a role for the api creator lambda function 132 | apicreator_lambda_role = iam.Role( 133 | scope=self, 134 | id="ApiCreatorLambdaRole", 135 | assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), 136 | managed_policies=[ 137 | iam.ManagedPolicy.from_aws_managed_policy_name( 138 | "service-role/AWSLambdaBasicExecutionRole" 139 | ) 140 | ] 141 | ) 142 | 143 | apicreator_lambda_role.add_to_policy( 144 | iam.PolicyStatement( 145 | effect=iam.Effect.ALLOW, 146 | resources=[ 147 | "arn:aws:apigateway:*::/apis/*", 148 | "arn:aws:apigateway:*::/apis" 149 | ], 150 | actions=[ 151 | "apigateway:DELETE", 152 | "apigateway:PUT", 153 | "apigateway:PATCH", 154 | "apigateway:POST", 155 | "apigateway:GET" 156 | ] 157 | ) 158 | ) 159 | 160 | apicreator_lambda_role.add_to_policy( 161 | iam.PolicyStatement( 162 | effect=iam.Effect.ALLOW, 163 | resources=["*"], 164 | actions=[ 165 | "logs:*" 166 | ] 167 | ) 168 | ) 169 | 170 | api_documentation_bucket.grant_read_write(apicreator_lambda_role) 171 | 172 | apicreator_lambda = aws_lambda.Function( 173 | scope=self, 174 | id="ApiCreatorLambda", 175 | code=aws_lambda.Code.from_asset( 176 | f"{os.path.dirname(__file__)}/resources/api_creation", 177 | bundling=BundlingOptions( 178 | image=aws_lambda.Runtime.PYTHON_3_9.bundling_image, 179 | command=[ 180 | "bash", "-c", 181 | "pip install --no-cache -r requirements.txt -t /asset-output && cp -au . /asset-output" 182 | ], 183 | ), 184 | ), 185 | handler="api_creator.lambda_handler", 186 | role=apicreator_lambda_role, 187 | runtime=aws_lambda.Runtime.PYTHON_3_9, 188 | timeout=Duration.minutes(5) 189 | ) 190 | 191 | # Provider that invokes the api creator lambda function 192 | apicreator_provider = custom_resources.Provider( 193 | self, 194 | 'ApiCreatorCustomResourceProvider', 195 | on_event_handler=apicreator_lambda 196 | ) 197 | 198 | # The custom resource that uses the api creator provider to supply values 199 | apicreator_custom_resource = CustomResource( 200 | self, 201 | 'ApiCreatorCustomResource', 202 | service_token=apicreator_provider.service_token, 203 | properties={ 204 | 'ApiGatewayAccessLogsLogGroupArn': api_gateway_access_log_group.log_group_arn, 205 | 'ApiIntegrationPingLambda': api_gateway_ping_lambda.function_arn, 206 | 'ApiIntegrationGreetingLambda': api_gateway_greeting_lambda.function_arn, 207 | 'ApiDocumentationBucketName': api_documentation_bucket.bucket_name, 208 | 'ApiDocumentationBucketUrl': api_documentation_bucket.bucket_website_url, 209 | 'ApiName': f"{config['api']['apiName']}", 210 | 'ApiStageName': config['api']['apiStageName'], 211 | 'ThrottlingBurstLimit': config['api']['throttlingBurstLimit'], 212 | 'ThrottlingRateLimit': config['api']['throttlingRateLimit'] 213 | } 214 | ) 215 | 216 | ########################################################## 217 | # Create API Creator Custom Resource 218 | ########################################################## 219 | 220 | ########################################################## 221 | # Create AWS API Gateway permissions 222 | ########################################################## 223 | 224 | apigateway_id = CustomResource.get_att_string(apicreator_custom_resource, attribute_name='ApiId') 225 | apigateway_endpoint = CustomResource.get_att_string(apicreator_custom_resource, attribute_name='ApiEndpoint') 226 | apigateway_stagename = CustomResource.get_att_string(apicreator_custom_resource, attribute_name='ApiStageName') 227 | 228 | http_api_arn = ( 229 | f"arn:{self.partition}:execute-api:" 230 | f"{self.region}:{self.account}:" 231 | f"{apigateway_id}/*/*/*" 232 | ) 233 | 234 | # grant HttpApi permission to invoke api lambda function 235 | api_gateway_ping_lambda.add_permission( 236 | f"Invoke By Orchestrator Gateway Permission", 237 | principal=iam.ServicePrincipal("apigateway.amazonaws.com"), 238 | action="lambda:InvokeFunction", 239 | source_arn=http_api_arn 240 | ) 241 | 242 | api_gateway_greeting_lambda.add_permission( 243 | f"Invoke By Orchestrator Gateway Permission", 244 | principal=iam.ServicePrincipal("apigateway.amazonaws.com"), 245 | action="lambda:InvokeFunction", 246 | source_arn=http_api_arn 247 | ) 248 | 249 | ########################################################## 250 | # Create AWS API Gateway permissions 251 | ########################################################## 252 | 253 | 254 | ########################################################## 255 | # Stack exports 256 | ########################################################## 257 | 258 | CfnOutput( 259 | self, 260 | id=f"api-gateway-dynamic-publish-id", 261 | value=apigateway_id, 262 | export_name="api-gateway-dynamic-publish-id" 263 | 264 | ) 265 | 266 | CfnOutput( 267 | self, 268 | id="api-gateway-dynamic-publish-endpoint", 269 | value=apigateway_endpoint, 270 | export_name="api-gateway-dynamic-publish-endpoint" 271 | ) 272 | 273 | CfnOutput( 274 | self, 275 | id="api-gateway-dynamic-publish-stagename", 276 | value=apigateway_stagename, 277 | export_name="api-gateway-dynamic-publish-stagename" 278 | ) 279 | 280 | CfnOutput( 281 | self, 282 | id="api-gateway-dynamic-publish-url", 283 | value=f"{apigateway_endpoint}/{apigateway_stagename}", 284 | export_name="api-gateway-dynamic-publish-url" 285 | ) 286 | 287 | CfnOutput( 288 | self, 289 | id="api-gateway-dynamic-publish-arn", 290 | value=http_api_arn, 291 | export_name="api-gateway-dynamic-publish-arn" 292 | ) 293 | 294 | CfnOutput( 295 | self, 296 | id="api-gateway-dynamic-publish-documentation-name", 297 | value=api_documentation_bucket.bucket_name, 298 | export_name="api-gateway-dynamic-publish-documentation-name" 299 | ) 300 | 301 | ########################################################## 302 | # Stack exports 303 | ########################################################## 304 | 305 | 306 | def read_cdk_context_json(self): 307 | filename = "cdk.json" 308 | 309 | with open(filename, 'r') as my_file: 310 | data = my_file.read() 311 | 312 | obj = json.loads(data) 313 | 314 | return obj.get('context') 315 | -------------------------------------------------------------------------------- /stacks/resources/api_creation/api_creator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | api_creator.py: 5 | Cloudformation custom resource lambda handler which performs the following tasks: 6 | * injects lambda functions arns (created during CDK deployment) into the 7 | OpenAPI 3 spec file (api_definition.yaml) 8 | * deploys or updates the API Gateway stage using the OpenAPI 3 spec file (api_definition.yaml) 9 | * deletes the API Gateway stage (if the Cloudformation operation is delete) 10 | """ 11 | 12 | import json 13 | import logging 14 | import os 15 | 16 | import boto3 17 | import yaml 18 | 19 | # set logging 20 | logger = logging.getLogger() 21 | logger.setLevel(logging.DEBUG) 22 | 23 | # environment variables 24 | aws_region = os.environ['AWS_REGION'] 25 | 26 | # boto3 clients 27 | apigateway_client = boto3.client('apigatewayv2') 28 | s3_client = boto3.client('s3') 29 | 30 | def replace_placeholders(template_file: str, substitutions: dict) -> str: 31 | import re 32 | 33 | def from_dict(dct): 34 | def lookup(match): 35 | key = match.group(1) 36 | return dct.get(key, f'<{key} not found>') 37 | return lookup 38 | 39 | with open (template_file, "r") as template_file: 40 | template_data = template_file.read() 41 | 42 | # perform the subsitutions, looking for placeholders @@PLACEHOLDER@@ 43 | api_template = re.sub('@@(.*?)@@', from_dict(substitutions), template_data) 44 | 45 | return api_template 46 | 47 | 48 | def get_api_by_name(api_name: str) -> str: 49 | get_apis = apigateway_client.get_apis() 50 | for api in get_apis['Items']: 51 | if api['Name'] == api_name: 52 | return api['ApiId'] 53 | 54 | return None 55 | 56 | 57 | def create_api(api_template: str) -> str: 58 | api_response = apigateway_client.import_api( 59 | Body=api_template, 60 | FailOnWarnings=True 61 | ) 62 | 63 | return api_response['ApiEndpoint'], api_response['ApiId'] 64 | 65 | 66 | def update_api(api_template: str, api_name: str) -> str: 67 | 68 | api_id = get_api_by_name(api_name) 69 | 70 | if api_id is not None: 71 | api_response = apigateway_client.reimport_api( 72 | ApiId=api_id, 73 | Body=api_template, 74 | FailOnWarnings=True 75 | ) 76 | return api_response['ApiEndpoint'], api_response['ApiId'] 77 | 78 | 79 | def delete_api(api_name: str) -> None: 80 | if get_api_by_name(api_name) is not None: 81 | apigateway_client.delete_api( 82 | ApiId=get_api_by_name(api_name) 83 | ) 84 | 85 | 86 | def deploy_api( 87 | api_id: str, 88 | api_stage_name: str, 89 | api_access_logs_arn: str, 90 | throttling_burst_limit: int, 91 | throttling_rate_limit: int 92 | ) -> None: 93 | apigateway_client.create_stage( 94 | AccessLogSettings={ 95 | 'DestinationArn': api_access_logs_arn, 96 | 'Format': '$context.identity.sourceIp - - [$context.requestTime] "$context.httpMethod $context.routeKey $context.protocol" $context.status $context.responseLength $context.requestId $context.integrationErrorMessage' 97 | }, 98 | ApiId=api_id, 99 | StageName=api_stage_name, 100 | AutoDeploy=True, 101 | DefaultRouteSettings={ 102 | 'DetailedMetricsEnabled': True, 103 | 'ThrottlingBurstLimit':throttling_burst_limit, 104 | 'ThrottlingRateLimit': throttling_rate_limit 105 | } 106 | ) 107 | 108 | 109 | def delete_api_deployment(api_id: str, api_stage_name: str) -> None: 110 | try: 111 | apigateway_client.get_stage( 112 | ApiId=api_id, 113 | StageName=api_stage_name 114 | ) 115 | 116 | apigateway_client.delete_stage( 117 | ApiId=api_id, 118 | StageName=api_stage_name 119 | ) 120 | except apigateway_client.exceptions.NotFoundException as e: 121 | logger.error(f"Stage name: {api_stage_name} for api id: {api_id} was not found during stage deletion. This is an expected error condition and is handled in code.") 122 | except Exception as e: 123 | raise ValueError(f"Unexpected error encountered during api deployment deletion: {str(e)}") 124 | 125 | 126 | def publish_api_documentation(bucket_name: str, api_definition: str) -> None: 127 | 128 | api_definition_json=json.dumps(yaml.safe_load(api_definition)) 129 | 130 | with open("/tmp/swagger.json", "w") as swagger_file: 131 | swagger_file.write(api_definition_json) 132 | 133 | # Upload the file 134 | try: 135 | 136 | s3_client.upload_file("/tmp/swagger.json", bucket_name, "swagger.json") 137 | 138 | except Exception as e: 139 | logging.error(str(e)) 140 | raise ValueError(str(e)) 141 | 142 | 143 | def lambda_handler(event, context): 144 | 145 | # print the event details 146 | logger.debug(json.dumps(event, indent=2)) 147 | 148 | props = event['ResourceProperties'] 149 | api_gateway_access_log_group_arn = props['ApiGatewayAccessLogsLogGroupArn'] 150 | api_integration_ping_lambda = props['ApiIntegrationPingLambda'] 151 | api_integration_greetings_lambda = props['ApiIntegrationGreetingLambda'] 152 | api_name = props['ApiName'] 153 | api_stage_name = props['ApiStageName'] 154 | api_documentation_bucket_name = props['ApiDocumentationBucketName'] 155 | throttling_burst_limit = int(props['ThrottlingBurstLimit']) 156 | throttling_rate_limit = int(props['ThrottlingRateLimit']) 157 | 158 | lambda_substitutions = { 159 | "API_NAME": api_name, 160 | "API_INTEGRATION_PING_LAMBDA": f"arn:aws:apigateway:{aws_region}:lambda:path/2015-03-31/functions/{api_integration_ping_lambda}/invocations", 161 | "API_INTEGRATION_GREETING_LAMBDA": f"arn:aws:apigateway:{aws_region}:lambda:path/2015-03-31/functions/{api_integration_greetings_lambda}/invocations" 162 | } 163 | 164 | api_template = replace_placeholders("api_definition.yaml", lambda_substitutions) 165 | 166 | if event['RequestType'] != 'Delete': 167 | 168 | if get_api_by_name(api_name) is None: 169 | 170 | logger.debug("Creating API") 171 | 172 | api_endpoint, api_id = create_api(api_template) 173 | 174 | deploy_api(api_id, api_stage_name, api_gateway_access_log_group_arn, throttling_burst_limit, throttling_rate_limit) 175 | 176 | publish_api_documentation(api_documentation_bucket_name, api_template) 177 | 178 | output = { 179 | 'PhysicalResourceId': f"generated-api", 180 | 'Data': { 181 | 'ApiEndpoint': api_endpoint, 182 | 'ApiId': api_id, 183 | 'ApiStageName': api_stage_name 184 | } 185 | } 186 | 187 | return output 188 | 189 | else: 190 | 191 | logger.debug("Updating API") 192 | 193 | api_endpoint, api_id = update_api(api_template, api_name) 194 | 195 | # delete and redeploy the stage after updating the api definition 196 | delete_api_deployment(api_id, api_stage_name) 197 | deploy_api(api_id, api_stage_name, api_gateway_access_log_group_arn, throttling_burst_limit, throttling_rate_limit) 198 | 199 | publish_api_documentation(api_documentation_bucket_name, api_template) 200 | 201 | output = { 202 | 'PhysicalResourceId': f"generated-api", 203 | 'Data': { 204 | 'ApiEndpoint': api_endpoint, 205 | 'ApiId': api_id, 206 | 'ApiStageName': api_stage_name 207 | } 208 | } 209 | 210 | return output 211 | 212 | if event['RequestType'] == 'Delete': 213 | 214 | logger.debug("Deleting API") 215 | 216 | if get_api_by_name(api_name) is not None: 217 | delete_api(api_name) 218 | 219 | output = { 220 | 'PhysicalResourceId': f"generated-api", 221 | 'Data': { 222 | 'ApiEndpoint': "Deleted", 223 | 'ApiId': "Deleted", 224 | 'ApiStageName': "Deleted" 225 | } 226 | } 227 | logger.info(output) 228 | 229 | return output 230 | -------------------------------------------------------------------------------- /stacks/resources/api_creation/api_definition.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: @@API_NAME@@ 4 | version: "v1.0" 5 | x-amazon-apigateway-request-validators: 6 | all: 7 | validateRequestBody: true 8 | validateRequestParameters: true 9 | params-only: 10 | validateRequestBody: false 11 | validateRequestParameters: true 12 | x-amazon-apigateway-request-validator: all 13 | paths: 14 | /ping: 15 | get: 16 | summary: "Simulates an API Ping" 17 | description: | 18 | ## Simulates an API Ping 19 | 20 | The purpose of this endpoint is to simulate a Ping request and respond with a Pong answer. 21 | 22 | ### Sample invocation 23 | 24 | ```bash 25 | #!/bin/bash 26 | 27 | # set the desired AWS region below 28 | AWS_REGION="us-east-1" 29 | 30 | STACK_NAME="ApiGatewayDynamicPublish" 31 | 32 | # Get the API Endpoint 33 | API_ENDPOINT_URL_EXPORT_NAME="api-gateway-dynamic-publish-url" 34 | API_ENDPOINT_URL=$(aws cloudformation --region ${AWS_REGION} describe-stacks --stack-name ${STACK_NAME} --query "Stacks[0].Outputs[?ExportName=='${API_ENDPOINT_URL_EXPORT_NAME}'].OutputValue" --output text) 35 | API_GATEWAY_URL="${API_ENDPOINT_URL}" 36 | 37 | ################################################ 38 | # TEST Ping API 39 | ################################################ 40 | 41 | echo "Testing GET ${API_GATEWAY_URL}/ping" 42 | API_RESPONSE=$(curl -sX GET ${API_GATEWAY_URL}/ping) 43 | 44 | echo "" 45 | echo ${API_RESPONSE} 46 | echo "" 47 | ``` 48 | 49 | ### Sample response 50 | 51 | ```json 52 | { 53 | "ping": "Pong" 54 | } 55 | ``` 56 | operationId: "pingIntegration" 57 | x-amazon-apigateway-request-validator: all 58 | responses: 59 | 200: 60 | description: "OK" 61 | content: 62 | application/json: 63 | schema: 64 | type: array 65 | items: 66 | $ref: "#/components/schemas/PingResponse" 67 | 500: 68 | description: "Internal Server Error" 69 | x-amazon-apigateway-integration: 70 | uri: @@API_INTEGRATION_PING_LAMBDA@@ 71 | payloadFormatVersion: "2.0" 72 | httpMethod: "POST" 73 | type: "aws_proxy" 74 | connectionType: "INTERNET" 75 | /greeting: 76 | get: 77 | summary: "Get a greeting message" 78 | description: | 79 | ## Get a greeting message 80 | 81 | The purpose of this endpoint is send a greeting string and receive a greeting message. 82 | 83 | ### Sample invocation 84 | 85 | ```bash 86 | #!/bin/bash 87 | 88 | # set the desired AWS region below 89 | AWS_REGION="us-east-1" 90 | 91 | # set the greeting you would like to send, ensure it is a single word with no spaces 92 | GREETING="world" 93 | 94 | STACK_NAME="ApiGatewayDynamicPublish" 95 | 96 | # Get the API Endpoint 97 | API_ENDPOINT_URL_EXPORT_NAME="api-gateway-dynamic-publish-url" 98 | API_ENDPOINT_URL=$(aws cloudformation --region ${AWS_REGION} describe-stacks --stack-name ${STACK_NAME} --query "Stacks[0].Outputs[?ExportName=='${API_ENDPOINT_URL_EXPORT_NAME}'].OutputValue" --output text) 99 | API_GATEWAY_URL="${API_ENDPOINT_URL}" 100 | 101 | ################################################ 102 | # TEST Greetings API 103 | ################################################ 104 | 105 | echo "Testing GET ${API_GATEWAY_URL}/greetings?greeting=${GREETING}" 106 | API_RESPONSE=$(curl -sX GET ${API_GATEWAY_URL}/greetings?greeting=${GREETING}) 107 | 108 | echo "" 109 | echo ${API_RESPONSE} | jq . 110 | echo "" 111 | ``` 112 | 113 | ### Sample response 114 | 115 | ```json 116 | { 117 | "greeting": "Hello World" 118 | } 119 | ``` 120 | operationId: "greetingIntegration" 121 | x-amazon-apigateway-request-validator: all 122 | parameters: 123 | - in: query 124 | name: greeting 125 | schema: 126 | type: string 127 | description: | 128 | A greeting string which the API will combine to form a greeting message 129 | responses: 130 | 200: 131 | description: "OK" 132 | content: 133 | application/json: 134 | schema: 135 | type: array 136 | items: 137 | $ref: "#/components/schemas/GreetingResponse" 138 | 500: 139 | description: "Internal Server Error" 140 | x-amazon-apigateway-integration: 141 | uri: @@API_INTEGRATION_GREETING_LAMBDA@@ 142 | payloadFormatVersion: "2.0" 143 | httpMethod: "POST" 144 | type: "aws_proxy" 145 | connectionType: "INTERNET" 146 | components: 147 | schemas: 148 | PingResponse: 149 | type: object 150 | properties: 151 | ping: 152 | type: string 153 | description: | 154 | Response to the ping request. 155 | GreetingResponse: 156 | type: object 157 | properties: 158 | greeting: 159 | type: string 160 | description: | 161 | The greeting response which concatenates the incoming greeting to form a greeting message. -------------------------------------------------------------------------------- /stacks/resources/api_creation/requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML==6.0 2 | -------------------------------------------------------------------------------- /stacks/resources/api_integrations/greeting.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | greeting.py: 5 | Simple Lambda Function handler that is the target for 6 | the API Gateway "Greeting" endpoint. 7 | """ 8 | 9 | import json 10 | import logging 11 | import traceback 12 | 13 | # set logging 14 | logger = logging.getLogger() 15 | logger.setLevel(logging.DEBUG) 16 | 17 | def lambda_handler(event, context): 18 | # read the event to json 19 | logger.debug(json.dumps(event, indent=2)) 20 | 21 | try: 22 | 23 | # verify that the api_key query parameter is provided 24 | if 'queryStringParameters' not in event or 'greeting' not in event['queryStringParameters']: 25 | raise ValueError(f"greeting is expected as a query parameter but it was not present in the request; {event['rawPath']}") 26 | 27 | message = {"greeting": f"Hello {event['queryStringParameters']['greeting']}"} 28 | 29 | return { 30 | 'statusCode': 200, 31 | 'body': json.dumps(message, indent=2), 32 | 'headers': {'Content-Type': 'application/json'} 33 | } 34 | 35 | except Exception as e: 36 | 37 | traceback.print_exception(type(e), value=e, tb=e.__traceback__) 38 | 39 | logger.error(f'Greeting error: {str(e)}') 40 | 41 | api_error = {"error": str(e)} 42 | 43 | return { 44 | 'statusCode': 500, 45 | 'body': json.dumps(api_error), 46 | 'headers': {'Content-Type': 'application/json'} 47 | } 48 | -------------------------------------------------------------------------------- /stacks/resources/api_integrations/ping.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | ping.py: 5 | Simple Lambda Function handler that is the target for 6 | the API Gateway "Ping" endpoint. 7 | """ 8 | 9 | import json 10 | import logging 11 | import traceback 12 | 13 | # set logging 14 | logger = logging.getLogger() 15 | logger.setLevel(logging.DEBUG) 16 | 17 | def lambda_handler(event, context): 18 | # read the event to json 19 | logger.debug(json.dumps(event, indent=2)) 20 | 21 | try: 22 | 23 | message = {"ping": "Pong"} 24 | 25 | return { 26 | 'statusCode': 200, 27 | 'body': json.dumps(message, indent=2), 28 | 'headers': {'Content-Type': 'application/json'} 29 | } 30 | 31 | except Exception as e: 32 | 33 | traceback.print_exception(type(e), value=e, tb=e.__traceback__) 34 | 35 | logger.error(f'Ping error: {str(e)}') 36 | 37 | api_error = {"error": str(e)} 38 | 39 | return { 40 | 'statusCode': 500, 41 | 'body': json.dumps(api_error), 42 | 'headers': {'Content-Type': 'application/json'} 43 | } 44 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/api-gateway-dynamic-publish/ff5af879e9080e9a659fb60c5f9eca1389981f8f/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_apigateway_dynamic_publish.py: -------------------------------------------------------------------------------- 1 | import aws_cdk as cdk 2 | from aws_cdk.assertions import Template 3 | from aws_cdk.assertions import Match 4 | 5 | from stacks.apigateway_dynamic_publish import ApiGatewayDynamicPublishStack 6 | 7 | 8 | def test_synthesizes_properly(): 9 | app = cdk.App() 10 | 11 | # Create the ProcessorStack. 12 | apigateway_dynamic_publish_stack = ApiGatewayDynamicPublishStack( 13 | app, 14 | "ApiGatewayDynamicPublishStack" 15 | ) 16 | 17 | # Prepare the stack for assertions. 18 | template = Template.from_stack(apigateway_dynamic_publish_stack) 19 | 20 | # Assert that we have the expected resources 21 | template.resource_count_is("AWS::S3::Bucket", 1) 22 | template.resource_count_is("AWS::KMS::Key", 1) 23 | template.resource_count_is("AWS::Lambda::Function", 5) 24 | template.resource_count_is("AWS::IAM::Role", 4) 25 | template.resource_count_is("AWS::IAM::Policy", 3) 26 | template.resource_count_is("AWS::Lambda::Permission", 2) 27 | template.resource_count_is("AWS::CloudFormation::CustomResource", 1) 28 | template.resource_count_is("Custom::S3AutoDeleteObjects", 1) 29 | template.resource_count_is("AWS::S3::BucketPolicy", 1) 30 | 31 | template.has_resource_properties( 32 | "AWS::IAM::Role", 33 | Match.object_equals( 34 | { 35 | "AssumeRolePolicyDocument": { 36 | "Statement": [ 37 | { 38 | "Action": "sts:AssumeRole", 39 | "Effect": "Allow", 40 | "Principal": { 41 | "Service": "lambda.amazonaws.com" 42 | } 43 | } 44 | ], 45 | "Version": "2012-10-17" 46 | }, 47 | "ManagedPolicyArns": [ 48 | { 49 | "Fn::Join": [ 50 | "", 51 | [ 52 | "arn:", 53 | { 54 | "Ref": "AWS::Partition" 55 | }, 56 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 57 | ] 58 | ] 59 | } 60 | ] 61 | } 62 | ) 63 | ) 64 | 65 | template.has_resource_properties( 66 | "AWS::IAM::Policy", 67 | Match.object_equals( 68 | { 69 | "PolicyDocument": { 70 | "Statement": [ 71 | { 72 | "Action": [ 73 | "apigateway:DELETE", 74 | "apigateway:PUT", 75 | "apigateway:PATCH", 76 | "apigateway:POST", 77 | "apigateway:GET" 78 | ], 79 | "Effect": "Allow", 80 | "Resource": [ 81 | "arn:aws:apigateway:*::/apis/*", 82 | "arn:aws:apigateway:*::/apis" 83 | ] 84 | }, 85 | { 86 | "Action": "logs:*", 87 | "Effect": "Allow", 88 | "Resource": "*" 89 | }, 90 | { 91 | "Action": [ 92 | "s3:GetObject*", 93 | "s3:GetBucket*", 94 | "s3:List*", 95 | "s3:DeleteObject*", 96 | "s3:PutObject", 97 | "s3:PutObjectLegalHold", 98 | "s3:PutObjectRetention", 99 | "s3:PutObjectTagging", 100 | "s3:PutObjectVersionTagging", 101 | "s3:Abort*" 102 | ], 103 | "Effect": "Allow", 104 | "Resource": [ 105 | { 106 | "Fn::GetAtt": [ 107 | Match.any_value(), 108 | "Arn" 109 | ] 110 | }, 111 | { 112 | "Fn::Join": [ 113 | "", 114 | [ 115 | { 116 | "Fn::GetAtt": [ 117 | Match.any_value(), 118 | "Arn" 119 | ] 120 | }, 121 | "/*" 122 | ] 123 | ] 124 | } 125 | ] 126 | }, 127 | { 128 | "Action": [ 129 | "kms:Decrypt", 130 | "kms:DescribeKey", 131 | "kms:Encrypt", 132 | "kms:ReEncrypt*", 133 | "kms:GenerateDataKey*" 134 | ], 135 | "Effect": "Allow", 136 | "Resource": { 137 | "Fn::GetAtt": [ 138 | Match.any_value(), 139 | "Arn" 140 | ] 141 | } 142 | } 143 | ], 144 | "Version": "2012-10-17" 145 | }, 146 | "PolicyName": Match.any_value(), 147 | "Roles": [ 148 | { 149 | "Ref": Match.any_value() 150 | } 151 | ] 152 | } 153 | ) 154 | ) 155 | 156 | template.has_resource_properties( 157 | "AWS::Lambda::Permission", 158 | Match.object_equals( 159 | { 160 | "Action": "lambda:InvokeFunction", 161 | "FunctionName": { 162 | "Fn::GetAtt": [ 163 | Match.any_value(), 164 | "Arn" 165 | ] 166 | }, 167 | "Principal": "apigateway.amazonaws.com", 168 | "SourceArn": { 169 | "Fn::Join": [ 170 | "", 171 | [ 172 | "arn:", 173 | { 174 | "Ref": "AWS::Partition" 175 | }, 176 | ":execute-api:", 177 | { 178 | "Ref": "AWS::Region" 179 | }, 180 | ":", 181 | { 182 | "Ref": "AWS::AccountId" 183 | }, 184 | ":", 185 | { 186 | "Fn::GetAtt": [ 187 | "ApiCreatorCustomResource", 188 | "ApiId" 189 | ] 190 | }, 191 | "/*/*/*" 192 | ] 193 | ] 194 | } 195 | } 196 | ) 197 | ) 198 | 199 | template.has_resource_properties( 200 | "AWS::CloudFormation::CustomResource", 201 | Match.object_equals( 202 | { 203 | "ServiceToken": { 204 | "Fn::GetAtt": [ 205 | Match.any_value(), 206 | "Arn" 207 | ] 208 | }, 209 | "ApiGatewayAccessLogsLogGroupArn": { 210 | "Fn::GetAtt": [ 211 | Match.any_value(), 212 | "Arn" 213 | ] 214 | }, 215 | "ApiIntegrationPingLambda": { 216 | "Fn::GetAtt": [ 217 | Match.any_value(), 218 | "Arn" 219 | ] 220 | }, 221 | "ApiIntegrationGreetingLambda": { 222 | "Fn::GetAtt": [ 223 | Match.any_value(), 224 | "Arn" 225 | ] 226 | }, 227 | "ApiDocumentationBucketName": { 228 | "Ref": Match.any_value() 229 | }, 230 | "ApiDocumentationBucketUrl": { 231 | "Fn::GetAtt": [ 232 | Match.any_value(), 233 | "WebsiteURL" 234 | ] 235 | }, 236 | "ApiName": Match.any_value(), 237 | "ApiStageName": Match.any_value(), 238 | "ThrottlingBurstLimit": Match.any_value(), 239 | "ThrottlingRateLimit": Match.any_value() 240 | } 241 | ) 242 | ) 243 | 244 | template.has_resource_properties( 245 | "Custom::S3AutoDeleteObjects", 246 | Match.object_equals( 247 | { 248 | "ServiceToken": { 249 | "Fn::GetAtt": [ 250 | Match.any_value(), 251 | "Arn" 252 | ] 253 | }, 254 | "BucketName": { 255 | "Ref": Match.any_value() 256 | } 257 | } 258 | ) 259 | ) 260 | 261 | template.has_resource_properties( 262 | "AWS::S3::BucketPolicy", 263 | Match.object_equals( 264 | { 265 | "Bucket": { 266 | "Ref": Match.any_value() 267 | }, 268 | "PolicyDocument": { 269 | "Statement": [ 270 | { 271 | "Action": [ 272 | "s3:GetBucket*", 273 | "s3:List*", 274 | "s3:DeleteObject*" 275 | ], 276 | "Effect": "Allow", 277 | "Principal": { 278 | "AWS": { 279 | "Fn::GetAtt": [ 280 | Match.any_value(), 281 | "Arn" 282 | ] 283 | } 284 | }, 285 | "Resource": [ 286 | { 287 | "Fn::GetAtt": [ 288 | Match.any_value(), 289 | "Arn" 290 | ] 291 | }, 292 | { 293 | "Fn::Join": [ 294 | "", 295 | [ 296 | { 297 | "Fn::GetAtt": [ 298 | Match.any_value(), 299 | "Arn" 300 | ] 301 | }, 302 | "/*" 303 | ] 304 | ] 305 | } 306 | ] 307 | } 308 | ], 309 | "Version": "2012-10-17" 310 | } 311 | } 312 | ) 313 | ) 314 | 315 | template.has_resource_properties( 316 | "Custom::S3AutoDeleteObjects", 317 | Match.object_equals( 318 | { 319 | "ServiceToken": { 320 | "Fn::GetAtt": [ 321 | Match.any_value(), 322 | "Arn" 323 | ] 324 | }, 325 | "BucketName": { 326 | "Ref": Match.any_value() 327 | } 328 | } 329 | ) 330 | ) 331 | 332 | template.has_resource_properties( 333 | "AWS::S3::Bucket", 334 | Match.object_equals( 335 | { 336 | "BucketEncryption": { 337 | "ServerSideEncryptionConfiguration": [ 338 | { 339 | "ServerSideEncryptionByDefault": { 340 | "KMSMasterKeyID": { 341 | "Fn::GetAtt": [ 342 | Match.any_value(), 343 | "Arn" 344 | ] 345 | }, 346 | "SSEAlgorithm": "aws:kms" 347 | } 348 | } 349 | ] 350 | }, 351 | "PublicAccessBlockConfiguration": { 352 | "BlockPublicAcls": True, 353 | "BlockPublicPolicy": True, 354 | "IgnorePublicAcls": True, 355 | "RestrictPublicBuckets": True 356 | }, 357 | "Tags": [ 358 | { 359 | "Key": "aws-cdk:auto-delete-objects", 360 | "Value": "true" 361 | } 362 | ] 363 | } 364 | ) 365 | ) 366 | 367 | template.has_resource_properties( 368 | "AWS::KMS::Key", 369 | Match.object_equals( 370 | { 371 | "KeyPolicy": { 372 | "Statement": [ 373 | { 374 | "Action": "kms:*", 375 | "Effect": "Allow", 376 | "Principal": { 377 | "AWS": { 378 | "Fn::Join": [ 379 | "", 380 | [ 381 | "arn:", 382 | { 383 | "Ref": "AWS::Partition" 384 | }, 385 | ":iam::", 386 | { 387 | "Ref": "AWS::AccountId" 388 | }, 389 | ":root" 390 | ] 391 | ] 392 | } 393 | }, 394 | "Resource": "*" 395 | } 396 | ], 397 | "Version": "2012-10-17" 398 | }, 399 | "Description": Match.any_value() 400 | } 401 | ) 402 | ) 403 | -------------------------------------------------------------------------------- /view_api_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################### 4 | # Script Name : view_api_docs.sh 5 | # Description : Downloads Swagger UI, incorporates the 6 | # api-gateway-dynamic-project OpenAPI v3 spec 7 | # file into Swagger UI and runs Swagger UI 8 | # via a local node.js server which, by default, 9 | # listens on http://localhost:12345 10 | # Args : 11 | # Author : Damian McDonald 12 | ################################################################### 13 | 14 | ### check if AWS credential variables are correctly set 15 | if [ -z "${AWS_ACCESS_KEY_ID}" ] 16 | then 17 | echo "AWS credential variable AWS_ACCESS_KEY_ID is empty." 18 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment." 19 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html" 20 | fi 21 | 22 | if [ -z "${AWS_SECRET_ACCESS_KEY}" ] 23 | then 24 | echo "AWS credential variable AWS_SECRET_ACCESS_KEY is empty." 25 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment." 26 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html" 27 | fi 28 | 29 | if [ -z "${AWS_DEFAULT_REGION}" ] 30 | then 31 | echo "AWS credential variable AWS_DEFAULT_REGION is empty." 32 | echo "Please see the guide below for instructions on how to configure your AWS CLI environment." 33 | echo "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html" 34 | fi 35 | ### check if AWS credential variables are correctly set 36 | 37 | # define the swagger ui version to download 38 | SWAGGER_UI_RELEASE=4.5.2 39 | SWAGGER_UI_RELEASE_URL=https://github.com/swagger-api/swagger-ui/archive/refs/tags/v${SWAGGER_UI_RELEASE}.zip 40 | 41 | # define the root directory 42 | ROOT_DIR=$PWD 43 | 44 | echo "Preparing documentation ..." 45 | 46 | # AWS Cloudformation stack name 47 | STACK_NAME="ApiGatewayDynamicPublish" 48 | 49 | echo "Grabbing Cloudformation exports for Stack Name: ${STACK_NAME}" 50 | 51 | # the s3 bucket name containing the api documentation 52 | DOC_BUCKET_EXPORT_NAME="api-gateway-dynamic-publish-documentation-name" 53 | BUCKET_NAME=$(aws cloudformation --region ${AWS_DEFAULT_REGION} describe-stacks --stack-name ${STACK_NAME} --query "Stacks[0].Outputs[?ExportName=='${DOC_BUCKET_EXPORT_NAME}'].OutputValue" --output text) 54 | 55 | # ask the user for permission to download swagger ui 56 | echo "" 57 | echo "##################################" 58 | echo "" 59 | echo "To view API documentation in the OpenAPI format, it is necessary to download Swagger UI." 60 | echo "Swagger UI is a third party, open source project licenced under the Apache License 2.0." 61 | echo "See https://github.com/swagger-api/swagger-ui" 62 | echo "" 63 | echo "##################################" 64 | echo "" 65 | 66 | while true; do 67 | read -p "Would you like to download Swagger UI to view the API documentation? [y/n]" yn 68 | case $yn in 69 | [Yy]* ) break;; 70 | [Nn]* ) exit;; 71 | * ) echo "Please answer y[yes] or n[no].";; 72 | esac 73 | done 74 | 75 | # create a tmp download directory 76 | mkdir -p ${ROOT_DIR}/apidocs/.tmp 77 | 78 | # create the swagger-ui directory 79 | mkdir -p ${ROOT_DIR}/apidocs/swagger-ui 80 | 81 | # grab the swagger ui release 82 | wget --quiet -O ${ROOT_DIR}/apidocs/.tmp/v${SWAGGER_UI_RELEASE}.zip ${SWAGGER_UI_RELEASE_URL} 83 | cd ${ROOT_DIR}/apidocs/.tmp 84 | 85 | # unzip the swagger ui release and copy the dist folder 86 | unzip -qq v${SWAGGER_UI_RELEASE}.zip 87 | cp -r ${ROOT_DIR}/apidocs/.tmp/swagger-ui-${SWAGGER_UI_RELEASE}/dist/ ${ROOT_DIR}/apidocs/swagger-ui 88 | 89 | # delete the tmp directory 90 | rm -fr ${ROOT_DIR}/apidocs/.tmp 91 | 92 | # grab the dynamic swagger.json file 93 | cd ${ROOT_DIR}/apidocs/swagger-ui 94 | aws s3 cp s3://${BUCKET_NAME}/swagger.json swagger.json --region ${AWS_DEFAULT_REGION} 95 | 96 | # replace the defeault json path with the swagger.json downloaded from s3 97 | sed 's,https://petstore.swagger.io/v2/swagger.json,swagger.json,g' index.html | tee index.html > /dev/null 2>&1 98 | 99 | cd ${ROOT_DIR}/apidocs 100 | 101 | # install the npm dependencies 102 | npm install express 103 | 104 | # run a local nodejs server to visualize the dynamic documentation 105 | node server.js 106 | 107 | # return to starting directory 108 | cd ${ROOT_DIR} --------------------------------------------------------------------------------