From 475b79c8df19c3cca0457cd5f0cded6e42701c21 Mon Sep 17 00:00:00 2001 From: MrDrHolmes Date: Sat, 1 Nov 2025 10:08:40 +0900 Subject: [PATCH 01/53] Initial commit --- .gitignore | 8 ++++++++ .npmignore | 6 ++++++ README.md | 12 ++++++++++++ jest.config.js | 8 ++++++++ lib/index.ts | 21 +++++++++++++++++++++ package.json | 24 ++++++++++++++++++++++++ test/cross-account.test.ts | 18 ++++++++++++++++++ tsconfig.json | 32 ++++++++++++++++++++++++++++++++ 8 files changed, 129 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 README.md create mode 100644 jest.config.js create mode 100644 lib/index.ts create mode 100644 package.json create mode 100644 test/cross-account.test.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f60797b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..c1d6d45 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/README.md b/README.md new file mode 100644 index 0000000..67c0cf8 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Welcome to your CDK TypeScript Construct Library project + +You should explore the contents of this project. It demonstrates a CDK Construct Library that includes a construct (`CrossAccount`) +which contains an Amazon SQS queue that is subscribed to an Amazon SNS topic. + +The construct defines an interface (`CrossAccountProps`) to configure the visibility timeout of the queue. + +## Useful commands + +* `npm run build` compile typescript to js +* `npm run watch` watch for changes and compile +* `npm run test` perform the jest unit tests diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..08263b8 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..7fccf7d --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,21 @@ +// import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +// import * as sqs from 'aws-cdk-lib/aws-sqs'; + +export interface CrossAccountProps { + // Define construct properties here +} + +export class CrossAccount extends Construct { + + constructor(scope: Construct, id: string, props: CrossAccountProps = {}) { + super(scope, id); + + // Define construct contents here + + // example resource + // const queue = new sqs.Queue(this, 'CrossAccountQueue', { + // visibilityTimeout: cdk.Duration.seconds(300) + // }); + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7214b5d --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "cross-account", + "version": "0.1.0", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "22.7.9", + "aws-cdk-lib": "2.215.0", + "constructs": "^10.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "typescript": "~5.6.3" + }, + "peerDependencies": { + "aws-cdk-lib": "2.215.0", + "constructs": "^10.0.0" + } +} \ No newline at end of file diff --git a/test/cross-account.test.ts b/test/cross-account.test.ts new file mode 100644 index 0000000..0dbcc3d --- /dev/null +++ b/test/cross-account.test.ts @@ -0,0 +1,18 @@ +// import * as cdk from 'aws-cdk-lib/core'; +// import { Template } from 'aws-cdk-lib/assertions'; +// import * as CrossAccount from '../lib/index'; + +// example test. To run these tests, uncomment this file along with the +// example resource in lib/index.ts +test('SQS Queue Created', () => { +// const app = new cdk.App(); +// const stack = new cdk.Stack(app, "TestStack"); +// // WHEN +// new CrossAccount.CrossAccount(stack, 'MyTestConstruct'); +// // THEN +// const template = Template.fromStack(stack); + +// template.hasResourceProperties('AWS::SQS::Queue', { +// VisibilityTimeout: 300 +// }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfc61bf --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "es2022" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "skipLibCheck": true, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} From 1d93070d58b41461c27116c9e253c30ce32f3b9c Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 1 Nov 2025 12:17:48 +0900 Subject: [PATCH 02/53] Add cross-account bucket construct --- README.md | 53 +- lib/index.ts | 21 - lib/kms/index.ts | 0 lib/s3/bucket.ts | 78 + lib/s3/index.ts | 1 + lib/s3/updater.ts | 0 package-lock.json | 4298 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 4422 insertions(+), 29 deletions(-) create mode 100644 lib/kms/index.ts create mode 100644 lib/s3/bucket.ts create mode 100644 lib/s3/index.ts create mode 100644 lib/s3/updater.ts create mode 100644 package-lock.json diff --git a/README.md b/README.md index 67c0cf8..f87d33a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,49 @@ -# Welcome to your CDK TypeScript Construct Library project +# cross-account resources -You should explore the contents of this project. It demonstrates a CDK Construct Library that includes a construct (`CrossAccount`) -which contains an Amazon SQS queue that is subscribed to an Amazon SNS topic. +This library contains resources that can be configured to be accessible across AWS accounts. +By default, CDK can't update the resource policies of resources in another account, so when +setting up cross-account access between resources, some manual configuration is required. -The construct defines an interface (`CrossAccountProps`) to configure the visibility timeout of the queue. +BUT NO MORE. +No more shall developers be forced to click through dashboard menus (or type CLI commands, +tedious in its own right) to simply access their S3 buckets (et al) from another account! +FREEDOM!! -## Useful commands +# Diagram -* `npm run build` compile typescript to js -* `npm run watch` watch for changes and compile -* `npm run test` perform the jest unit tests +```mermaid + +architecture-beta + group accessor(logos:aws-cloud)[Accessor] + service cf(logos:aws-cloudfront)[CloudFront] in accessor + service s3-mgmt(logos:aws-lambda)[S3 Updater] in accessor + service kms-mgmt(logos:aws-lambda)[KMS Updater] in accessor + junction to-mgmt in accessor + + group accessed(logos:aws-cloud)[Accessed] + group s3(logos:aws-s3)[S3] in accessed + service bucket(logos:aws-s3)[Bucket] in s3 + service bucket-role(logos:aws-iam)[Bucket Updater Role] in s3 + group kms(logos:aws-kms)[KMS] in accessed + service key(logos:aws-kms)[Key] in kms + service key-role(logos:aws-iam)[Key Updater Role] in kms + + cf:R -[on update]- L:to-mgmt + + to-mgmt:T -- B:s3-mgmt + to-mgmt:B -- T:kms-mgmt + + s3-mgmt:R -[assume role]- L:bucket-role + bucket-role:R -[assume role]-> L:bucket + + kms-mgmt:R -[update policy]- L:key-role + key-role:R -[update policy]-> L:key + + key:T -[encryption]- B:bucket + +``` + +(One of these days GitHub will support logos:aws-icons in its inline Mermaid diagrams, thus +I'm using them now. +If they still aren't rendering properly by the time you read this, here's a link to a prerendered +.svg diagram as well.) diff --git a/lib/index.ts b/lib/index.ts index 7fccf7d..e69de29 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,21 +0,0 @@ -// import * as cdk from 'aws-cdk-lib'; -import { Construct } from 'constructs'; -// import * as sqs from 'aws-cdk-lib/aws-sqs'; - -export interface CrossAccountProps { - // Define construct properties here -} - -export class CrossAccount extends Construct { - - constructor(scope: Construct, id: string, props: CrossAccountProps = {}) { - super(scope, id); - - // Define construct contents here - - // example resource - // const queue = new sqs.Queue(this, 'CrossAccountQueue', { - // visibilityTimeout: cdk.Duration.seconds(300) - // }); - } -} diff --git a/lib/kms/index.ts b/lib/kms/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/lib/s3/bucket.ts b/lib/s3/bucket.ts new file mode 100644 index 0000000..803c99e --- /dev/null +++ b/lib/s3/bucket.ts @@ -0,0 +1,78 @@ +import { Construct } from "constructs"; +import { CfnOutput } from "aws-cdk-lib"; +import { Bucket, BucketProps } from "aws-cdk-lib/aws-s3"; +import { + AccountRootPrincipal, + ArnPrincipal, + Effect, + PolicyDocument, + PolicyStatement, + Role, +} from "aws-cdk-lib/aws-iam"; + +export interface CrossAccountS3BucketProps extends BucketProps { + xaAwsIds: string[]; +} + +/** + * CrossAccountS3Bucket creates an S3 bucket with a corresponding IAM role + * that can be assumed by specific cross-account roles to update the bucket policy. + * + * @remarks + * - `xaAwsIds` should be the list of AWS account IDs that are allowed to assume + * the management role. + * - The IAM role created is scoped to `s3:GetBucketPolicy` and `s3:PutBucketPolicy` + * on the bucket only. + */ +export class CrossAccountS3Bucket extends Construct { + /** The managed S3 bucket */ + public readonly bucket: Bucket; + + /** The IAM role used for cross-account policy management */ + public readonly role: Role; + + constructor(scope: Construct, id: string, props: CrossAccountS3BucketProps) { + super(scope, id); + + const { xaAwsIds, ...bucketProps } = props; + + this.bucket = new Bucket(this, "xa-bucket", bucketProps); + + this.role = new Role(this, "xa-mgmt-role", { + assumedBy: new AccountRootPrincipal(), // placeholder + description: `IAM role to enable cross-account management of policy for ${this.bucket.bucketName}`, + roleName: `${this.bucket.bucketName}-xa-mgmt`, + inlinePolicies: { + UpdateBucketPolicy: new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ["s3:GetBucketPolicy", "s3:PutBucketPolicy"], + resources: [this.bucket.bucketArn], + }), + ], + }), + }, + }); + for (const xaAwsId of xaAwsIds) { + const roleName = `${this.bucket.bucketName}-xa-mgmt-execution-role`.slice( + 0, + 64, + ); + this.role.grantAssumeRole( + new ArnPrincipal(`arn:aws:iam::${xaAwsId}:role/${roleName}`), + ); + } + + new CfnOutput(this, "bucket-name", { + value: this.bucket.bucketName, + description: "Name of the cross-account managed S3 bucket", + }); + + new CfnOutput(this, "xa-mgmt-role-arn", { + value: this.role.roleArn, + description: + "ARN of the IAM role used for cross-account bucket policy management", + }); + } +} diff --git a/lib/s3/index.ts b/lib/s3/index.ts new file mode 100644 index 0000000..9a4172d --- /dev/null +++ b/lib/s3/index.ts @@ -0,0 +1 @@ +export { CrossAccountS3Bucket, CrossAccountS3BucketProps } from "./bucket"; diff --git a/lib/s3/updater.ts b/lib/s3/updater.ts new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4138ae3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4298 @@ +{ + "name": "cross-account", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cross-account", + "version": "0.1.0", + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "22.7.9", + "aws-cdk-lib": "2.215.0", + "constructs": "^10.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "typescript": "~5.6.3" + }, + "peerDependencies": { + "aws-cdk-lib": "2.215.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.242", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.242.tgz", + "integrity": "sha512-4c1bAy2ISzcdKXYS1k4HYZsNrgiwbiDzj36ybwFVxEWZXVAP0dimQTCaB9fxu7sWzEjw3d+eaw6Fon+QTfTIpQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "48.17.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-48.17.0.tgz", + "integrity": "sha512-k1nNmluSq3Pf7N6ljgVRw2wyZLbdODCSszN+UdvXMIC8jnU08ak5DSjioLhGUSPP+yjZzuxMnS27kY9DqydRjA==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.2" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "22.7.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", + "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.34", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", + "integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.215.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.215.0.tgz", + "integrity": "sha512-DvRiUEsZSc4voeqfkYGrjP0f4NJ1u94JvbVCUQ+Fci4rf25xLRSEVPzyaH9Tf7V38i0Otts1+u7seunqe+R19g==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.242", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^48.6.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.1", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.7.2", + "table": "^6.9.0", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.17.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "dev": true, + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.1.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.22", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.22.tgz", + "integrity": "sha512-/tk9kky/d8T8CTXIQYASLyhAxR5VwL3zct1oAoVTaOUHwrmsGnfbRwNdEq+vOl2BN8i3PcDdP0o4Q+jjKQoFbQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001752", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001752.tgz", + "integrity": "sha512-vKUk7beoukxE47P5gcVNKkDRzXdVofotshHwfR9vmpeFKxmI5PBpgOMC18LUJUA/DvJ70Y7RveasIBraqsyO/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/constructs": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", + "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.244", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", + "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", + "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} From 02d081feb0d15e903e0ac31aa0570fe9281f75c6 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 1 Nov 2025 16:24:52 +0900 Subject: [PATCH 03/53] Add updater construct --- .gitignore | 2 + lib/s3/bucket.ts | 2 +- lib/s3/lambda-code/main.py | 13 ++++ lib/s3/updater.ts | 141 +++++++++++++++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 lib/s3/lambda-code/main.py diff --git a/.gitignore b/.gitignore index f60797b..4fb79df 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ node_modules # CDK asset staging directory .cdk.staging cdk.out + +**/.venv diff --git a/lib/s3/bucket.ts b/lib/s3/bucket.ts index 803c99e..df86f54 100644 --- a/lib/s3/bucket.ts +++ b/lib/s3/bucket.ts @@ -41,7 +41,7 @@ export class CrossAccountS3Bucket extends Construct { this.role = new Role(this, "xa-mgmt-role", { assumedBy: new AccountRootPrincipal(), // placeholder description: `IAM role to enable cross-account management of policy for ${this.bucket.bucketName}`, - roleName: `${this.bucket.bucketName}-xa-mgmt`, + roleName: `${this.bucket.bucketName}-xa-mgmt`.slice(0, 64), inlinePolicies: { UpdateBucketPolicy: new PolicyDocument({ statements: [ diff --git a/lib/s3/lambda-code/main.py b/lib/s3/lambda-code/main.py new file mode 100644 index 0000000..3e19b28 --- /dev/null +++ b/lib/s3/lambda-code/main.py @@ -0,0 +1,13 @@ +import os +import logging +import boto3 + +s3 = boto3.client("s3") + +# Initialize the logger +logger = logging.getLogger() +logger.setLevel("INFO") + + +def handler(event, context): + pass diff --git a/lib/s3/updater.ts b/lib/s3/updater.ts index e69de29..d9556af 100644 --- a/lib/s3/updater.ts +++ b/lib/s3/updater.ts @@ -0,0 +1,141 @@ +import { Duration } from "aws-cdk-lib"; +import { + Effect, + PolicyDocument, + PolicyStatement, + Role, + ServicePrincipal, +} from "aws-cdk-lib/aws-iam"; +import { Code, Function, Runtime } from "aws-cdk-lib/aws-lambda"; +import { + AwsCustomResource, + AwsCustomResourcePolicy, +} from "aws-cdk-lib/custom-resources"; +import { Construct } from "constructs"; +import path from "path"; + +interface Registry { + [bucketName: string]: { [service: string]: Set }; +} + +export interface CrossAccountS3BucketManagerProps { + bucketName: string; + bucketAwsId: string; + managerTimeout?: number; + callerTimeout?: number; +} + +/** + * CrossAccountS3BucketManager creates a Lambda function and calls it at deployment + * time using an AwsCustomResource (Lambda:InvokeFunction). + * + * @remarks + * - `bucketName` should be the name of the S3 bucket in the other account whose + * policy we want to manage. + * - `bucketAwsId` should be the ID of the AWS account the aforementioned bucket + * belongs to. + * - `managerTimeout` is optional and specifies the number of seconds for the manager + * Lambda Function's timeout (defaults to 30). + * - `callerTimeout` is optional and specifies the number of seconds for the + * AwsCustomResource's timeout (defaults to 30). + * - To use this construct, first register the resources that need to access a given + * S3 Bucket using one of the following static registry functions: + * - allowCloudfront(bucketName, cloudfrontId) + */ +export class CrossAccountS3BucketManager extends Construct { + /** Static registry mapping bucketNames to services to resource IDs that need access */ + private static registry: Registry = {}; + + /** Returns a sorted list of the set of Cloudfront Distribution IDs in the registry */ + private static getCloudfrontAccessors(bucketName: string): string[] { + return [...(this.registry[bucketName]?.["cloudfront"] ?? [])].sort(); + } + + /** + * The Lambda Function that will be called to assume a role cross-account and + * manage the S3 Bucket + */ + public readonly function: Function; + + constructor( + scope: Construct, + id: string, + props: CrossAccountS3BucketManagerProps, + ) { + super(scope, id); + + const { + bucketName, + bucketAwsId, + managerTimeout = 30, + callerTimeout = 30, + } = props; + + const assumeXaMgmtRole = new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ["sts:AssumeRole"], + resources: [ + `arn:aws:iam::${bucketAwsId}:role/${bucketName}-xa-mgmt`.slice( + 0, + 64, + ), + ], + }), + ], + }); + + const role = new Role(this, "xa-mgmt-lambda-role", { + assumedBy: new ServicePrincipal("lambda.amazonaws.com"), + description: `Execution role for ${bucketName} Lambda function.`, + inlinePolicies: { + AssumeXaMgmtRole: assumeXaMgmtRole, + }, + }); + + this.function = new Function(this, "xa-mgmt-lambda", { + code: Code.fromAsset(path.join(__dirname, "lambda-code")), + handler: "main.handler", + runtime: Runtime.PYTHON_3_13, + timeout: Duration.seconds(managerTimeout), // default timeout of 3 seconds is awful short/fragile + role, + }); + + const cloudfrontDistributionIds = + CrossAccountS3BucketManager.getCloudfrontAccessors(bucketName); + + const callFor = (operation: string) => { + return { + service: "Lambda", + action: "InvokeFunction", + parameters: { + FunctionName: this.function.functionName, + InvocationType: "Event", + Payload: JSON.stringify({ + operation, + cloudfrontDistributionIds, + }), + }, + }; + }; + + const caller = new AwsCustomResource(this, "xa-mgmt-lambda-caller", { + onCreate: callFor("create"), + onUpdate: callFor("update"), + onDelete: callFor("delete"), + policy: AwsCustomResourcePolicy.fromSdkCalls({ + resources: [this.function.functionArn], + }), + timeout: Duration.seconds(callerTimeout), + }); + } + + public static allowCloudfront(bucketName: string, cloudfrontId: string) { + if (!(bucketName in this.registry)) { + this.registry[bucketName] = {}; + this.registry[bucketName]["cloudfront"] = new Set(); + } + this.registry[bucketName]["cloudfront"].add(cloudfrontId); + } +} From 4a62a89c509c6578c875c183dc93d9a4080824e9 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sun, 2 Nov 2025 08:43:19 +0900 Subject: [PATCH 04/53] Template tests for s3 bucket --- lib/index.ts | 1 + lib/s3/bucket.ts | 30 ++++-- lib/s3/index.ts | 3 +- lib/s3/updater.ts | 8 +- package-lock.json | 1 + package.json | 2 +- test/cross-account-s3.test.ts | 166 ++++++++++++++++++++++++++++++++++ test/cross-account.test.ts | 18 ---- tsconfig.json | 14 +-- 9 files changed, 197 insertions(+), 46 deletions(-) create mode 100644 test/cross-account-s3.test.ts delete mode 100644 test/cross-account.test.ts diff --git a/lib/index.ts b/lib/index.ts index e69de29..307f60b 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -0,0 +1 @@ +export { CrossAccountS3Bucket } from "./s3"; diff --git a/lib/s3/bucket.ts b/lib/s3/bucket.ts index df86f54..fe89116 100644 --- a/lib/s3/bucket.ts +++ b/lib/s3/bucket.ts @@ -36,12 +36,20 @@ export class CrossAccountS3Bucket extends Construct { const { xaAwsIds, ...bucketProps } = props; + // The updater Lambda's execution role will have 11 characters appended to it + // so let's keep the bucket name to 52 characters or less + if ((bucketProps.bucketName?.length ?? 0) > 52) { + throw new Error( + `Bucket name "${bucketProps.bucketName}" must be 52 characters or less.`, + ); + } + this.bucket = new Bucket(this, "xa-bucket", bucketProps); this.role = new Role(this, "xa-mgmt-role", { assumedBy: new AccountRootPrincipal(), // placeholder description: `IAM role to enable cross-account management of policy for ${this.bucket.bucketName}`, - roleName: `${this.bucket.bucketName}-xa-mgmt`.slice(0, 64), + roleName: `${this.bucket.bucketName}-xa-mgmt`, inlinePolicies: { UpdateBucketPolicy: new PolicyDocument({ statements: [ @@ -54,15 +62,17 @@ export class CrossAccountS3Bucket extends Construct { }), }, }); - for (const xaAwsId of xaAwsIds) { - const roleName = `${this.bucket.bucketName}-xa-mgmt-execution-role`.slice( - 0, - 64, - ); - this.role.grantAssumeRole( - new ArnPrincipal(`arn:aws:iam::${xaAwsId}:role/${roleName}`), - ); - } + + const roleName = `${this.bucket.bucketName}-xa-mgmt-ex`; + this.role.assumeRolePolicy?.addStatements( + new PolicyStatement({ + effect: Effect.ALLOW, + principals: xaAwsIds.map( + (id) => new ArnPrincipal(`arn:aws:iam::${id}:role/${roleName}`), + ), + actions: ["sts:AssumeRole"], + }), + ); new CfnOutput(this, "bucket-name", { value: this.bucket.bucketName, diff --git a/lib/s3/index.ts b/lib/s3/index.ts index 9a4172d..c8af618 100644 --- a/lib/s3/index.ts +++ b/lib/s3/index.ts @@ -1 +1,2 @@ -export { CrossAccountS3Bucket, CrossAccountS3BucketProps } from "./bucket"; +export { CrossAccountS3Bucket } from "./bucket"; +export type { CrossAccountS3BucketProps } from "./bucket"; diff --git a/lib/s3/updater.ts b/lib/s3/updater.ts index d9556af..25c523a 100644 --- a/lib/s3/updater.ts +++ b/lib/s3/updater.ts @@ -76,12 +76,7 @@ export class CrossAccountS3BucketManager extends Construct { new PolicyStatement({ effect: Effect.ALLOW, actions: ["sts:AssumeRole"], - resources: [ - `arn:aws:iam::${bucketAwsId}:role/${bucketName}-xa-mgmt`.slice( - 0, - 64, - ), - ], + resources: [`arn:aws:iam::${bucketAwsId}:role/${bucketName}-xa-mgmt`], }), ], }); @@ -92,6 +87,7 @@ export class CrossAccountS3BucketManager extends Construct { inlinePolicies: { AssumeXaMgmtRole: assumeXaMgmtRole, }, + roleName: `${bucketName}-xa-mgmt-ex`, }); this.function = new Function(this, "xa-mgmt-lambda", { diff --git a/package-lock.json b/package-lock.json index 4138ae3..55fffbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -105,6 +105,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", diff --git a/package.json b/package.json index 7214b5d..2ce5f5f 100644 --- a/package.json +++ b/package.json @@ -21,4 +21,4 @@ "aws-cdk-lib": "2.215.0", "constructs": "^10.0.0" } -} \ No newline at end of file +} diff --git a/test/cross-account-s3.test.ts b/test/cross-account-s3.test.ts new file mode 100644 index 0000000..293050c --- /dev/null +++ b/test/cross-account-s3.test.ts @@ -0,0 +1,166 @@ +import { App, Stack } from "aws-cdk-lib/core"; +import { Match, Template } from "aws-cdk-lib/assertions"; +import { CrossAccountS3Bucket } from "../lib"; + +const getRoleNameMatcher = (bucketId: string) => + Match.objectEquals({ + "Fn::Join": Match.arrayEquals([ + "", + Match.arrayEquals([ + Match.objectEquals({ + Ref: Match.stringLikeRegexp(`^${bucketId.replaceAll("-", "")}`), + }), + "-xa-mgmt", + ]), + ]), + }); + +const getAwsPrincipalMatcher = (bucketName: string, id: string) => + Match.objectEquals({ + "Fn::Join": Match.arrayEquals([ + "", + Match.arrayEquals([ + `arn:aws:iam::${id}:role/`, + Match.objectEquals({ + Ref: Match.stringLikeRegexp(`^${bucketName.replaceAll("-", "")}`), + }), + "-xa-mgmt-ex", + ]), + ]), + }); + +const getAssumeRolePolicyMatcher = (bucketName: string, xaAwsIds: string[]) => + Match.objectLike({ + Statement: Match.arrayWith([ + Match.objectLike({ + Action: "sts:AssumeRole", + Principal: Match.objectLike({ + AWS: + xaAwsIds.length == 1 + ? getAwsPrincipalMatcher(bucketName, xaAwsIds[0]) + : xaAwsIds.map((id) => getAwsPrincipalMatcher(bucketName, id)), + }), + }), + ]), + Version: "2012-10-17", + }); + +test("Single Accessor XAS3", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const bucketId = "test-xas3-single-accessor"; + const bucketName = "test-xas3-s"; + const xaAwsIds = ["000000000000"]; + // WHEN + new CrossAccountS3Bucket(stack, bucketId, { + bucketName, + xaAwsIds, + }); + // THEN + const template = Template.fromStack(stack); + + template.hasResourceProperties("AWS::S3::Bucket", { + BucketName: "test-xas3-s", + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: getRoleNameMatcher(bucketId), + AssumeRolePolicyDocument: getAssumeRolePolicyMatcher(bucketName, xaAwsIds), + }); +}); + +test("Multi Accessor XAS3", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const bucketId = "test-xas3-multi-accessor"; + const bucketName = "test-xas3-m"; + const xaAwsIds = ["000000000000", "111111111111", "222222222222"]; + // WHEN + new CrossAccountS3Bucket(stack, bucketId, { + bucketName, + xaAwsIds, + }); + // THEN + const template = Template.fromStack(stack); + + template.hasResourceProperties("AWS::S3::Bucket", { + BucketName: "test-xas3-m", + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: getRoleNameMatcher(bucketId), + AssumeRolePolicyDocument: getAssumeRolePolicyMatcher(bucketName, xaAwsIds), + }); +}); + +test("Longish XAS3", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const bucketId = "test-xas3-loooooooooooong"; + const looongBucketName = + "test-xas3-loooooooooooooooooooooooooooooooooooongish"; + // WHEN + new CrossAccountS3Bucket(stack, bucketId, { + bucketName: looongBucketName, + xaAwsIds: ["000000000000"], + }); + // THEN + const template = Template.fromStack(stack); + + template.hasResourceProperties("AWS::S3::Bucket", { + BucketName: looongBucketName, + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: getRoleNameMatcher(bucketId), + }); +}); + +test("Loooooooooooong XAS3", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const bucketId = "test-xas3-loooooooooooong"; + const looongBucketName = + "test-xas3-loooooooooo00000000000000000000000000oooooooooooooong"; + + expect( + () => + new CrossAccountS3Bucket(stack, bucketId, { + bucketName: looongBucketName, + xaAwsIds: ["000000000000"], + }), + ).toThrow(/must be 52 characters or less/); +}); + +test("Several XAS3s", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const bucketId1 = "test-xas3-1"; + const xaAwsIds1 = ["000000000000"]; + const bucketId2 = "test-xas3-2"; + const xaAwsIds2 = ["111111111111"]; + // WHEN + new CrossAccountS3Bucket(stack, bucketId1, { + bucketName: bucketId1, + xaAwsIds: xaAwsIds1, + }); + new CrossAccountS3Bucket(stack, bucketId2, { + bucketName: bucketId2, + xaAwsIds: xaAwsIds2, + }); + // THEN + const template = Template.fromStack(stack); + + // Do me later + template.hasResourceProperties("AWS::S3::Bucket", { + BucketName: bucketId1, + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: getRoleNameMatcher(bucketId1), + AssumeRolePolicyDocument: getAssumeRolePolicyMatcher(bucketId1, xaAwsIds1), + }); + template.hasResourceProperties("AWS::S3::Bucket", { + BucketName: bucketId2, + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: getRoleNameMatcher(bucketId2), + AssumeRolePolicyDocument: getAssumeRolePolicyMatcher(bucketId2, xaAwsIds2), + }); +}); diff --git a/test/cross-account.test.ts b/test/cross-account.test.ts deleted file mode 100644 index 0dbcc3d..0000000 --- a/test/cross-account.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -// import * as cdk from 'aws-cdk-lib/core'; -// import { Template } from 'aws-cdk-lib/assertions'; -// import * as CrossAccount from '../lib/index'; - -// example test. To run these tests, uncomment this file along with the -// example resource in lib/index.ts -test('SQS Queue Created', () => { -// const app = new cdk.App(); -// const stack = new cdk.Stack(app, "TestStack"); -// // WHEN -// new CrossAccount.CrossAccount(stack, 'MyTestConstruct'); -// // THEN -// const template = Template.fromStack(stack); - -// template.hasResourceProperties('AWS::SQS::Queue', { -// VisibilityTimeout: 300 -// }); -}); diff --git a/tsconfig.json b/tsconfig.json index bfc61bf..777f492 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,9 +3,8 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "lib": [ - "es2022" - ], + "isolatedModules": true, + "lib": ["es2022"], "declaration": true, "strict": true, "noImplicitAny": true, @@ -21,12 +20,7 @@ "experimentalDecorators": true, "strictPropertyInitialization": false, "skipLibCheck": true, - "typeRoots": [ - "./node_modules/@types" - ] + "typeRoots": ["./node_modules/@types"] }, - "exclude": [ - "node_modules", - "cdk.out" - ] + "exclude": ["node_modules", "cdk.out"] } From bb4e6b9b144408fa9afc6933fb69105006e5db88 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sun, 2 Nov 2025 11:16:16 +0900 Subject: [PATCH 05/53] S3 manager unit tests --- lib/index.ts | 6 +- lib/s3/index.ts | 3 + lib/s3/updater.ts | 41 ++-- ...est.ts => cross-account-s3-bucket.test.ts} | 0 test/cross-account-s3-updater.test.ts | 202 ++++++++++++++++++ 5 files changed, 237 insertions(+), 15 deletions(-) rename test/{cross-account-s3.test.ts => cross-account-s3-bucket.test.ts} (100%) create mode 100644 test/cross-account-s3-updater.test.ts diff --git a/lib/index.ts b/lib/index.ts index 307f60b..f9c60a1 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1 +1,5 @@ -export { CrossAccountS3Bucket } from "./s3"; +export { CrossAccountS3Bucket, CrossAccountS3BucketManager } from "./s3"; +export type { + CrossAccountS3BucketProps, + CrossAccountS3BucketManagerProps, +} from "./s3"; diff --git a/lib/s3/index.ts b/lib/s3/index.ts index c8af618..2d21ea1 100644 --- a/lib/s3/index.ts +++ b/lib/s3/index.ts @@ -1,2 +1,5 @@ export { CrossAccountS3Bucket } from "./bucket"; export type { CrossAccountS3BucketProps } from "./bucket"; + +export { CrossAccountS3BucketManager } from "./updater"; +export type { CrossAccountS3BucketManagerProps } from "./updater"; diff --git a/lib/s3/updater.ts b/lib/s3/updater.ts index 25c523a..51d9ab1 100644 --- a/lib/s3/updater.ts +++ b/lib/s3/updater.ts @@ -10,12 +10,13 @@ import { Code, Function, Runtime } from "aws-cdk-lib/aws-lambda"; import { AwsCustomResource, AwsCustomResourcePolicy, + PhysicalResourceId, } from "aws-cdk-lib/custom-resources"; import { Construct } from "constructs"; import path from "path"; interface Registry { - [bucketName: string]: { [service: string]: Set }; + [bucketName: string]: Set | false; } export interface CrossAccountS3BucketManagerProps { @@ -43,12 +44,21 @@ export interface CrossAccountS3BucketManagerProps { * - allowCloudfront(bucketName, cloudfrontId) */ export class CrossAccountS3BucketManager extends Construct { - /** Static registry mapping bucketNames to services to resource IDs that need access */ - private static registry: Registry = {}; + /** Static registry mapping bucketNames to CloudFront Distribution IDs that need access */ + private static cloudfrontRegistry: Registry = {}; - /** Returns a sorted list of the set of Cloudfront Distribution IDs in the registry */ - private static getCloudfrontAccessors(bucketName: string): string[] { - return [...(this.registry[bucketName]?.["cloudfront"] ?? [])].sort(); + /** + * Returns a sorted list of the set of Cloudfront Distribution IDs in the registry and + * sets the manager to created + */ + private static consumeCloudfrontAccessors(bucketName: string): string[] { + const accessors = [ + ...(this.cloudfrontRegistry[bucketName] + ? this.cloudfrontRegistry[bucketName] + : []), + ].sort(); + this.cloudfrontRegistry[bucketName] = false; + return accessors; } /** @@ -67,8 +77,8 @@ export class CrossAccountS3BucketManager extends Construct { const { bucketName, bucketAwsId, - managerTimeout = 30, - callerTimeout = 30, + managerTimeout = 30, // default timeout of 3 seconds is awful short/fragile + callerTimeout = 30, // default timeout of 3 seconds is awful short/fragile } = props; const assumeXaMgmtRole = new PolicyDocument({ @@ -94,15 +104,16 @@ export class CrossAccountS3BucketManager extends Construct { code: Code.fromAsset(path.join(__dirname, "lambda-code")), handler: "main.handler", runtime: Runtime.PYTHON_3_13, - timeout: Duration.seconds(managerTimeout), // default timeout of 3 seconds is awful short/fragile + timeout: Duration.seconds(managerTimeout), role, }); const cloudfrontDistributionIds = - CrossAccountS3BucketManager.getCloudfrontAccessors(bucketName); + CrossAccountS3BucketManager.consumeCloudfrontAccessors(bucketName); const callFor = (operation: string) => { return { + physicalResourceId: PhysicalResourceId.of("xa-mgmt-lambda-caller"), service: "Lambda", action: "InvokeFunction", parameters: { @@ -128,10 +139,12 @@ export class CrossAccountS3BucketManager extends Construct { } public static allowCloudfront(bucketName: string, cloudfrontId: string) { - if (!(bucketName in this.registry)) { - this.registry[bucketName] = {}; - this.registry[bucketName]["cloudfront"] = new Set(); + if (this.cloudfrontRegistry[bucketName] === false) { + throw new Error( + `Cannot register resources for bucket ${bucketName} manager after creation.`, + ); } - this.registry[bucketName]["cloudfront"].add(cloudfrontId); + this.cloudfrontRegistry[bucketName] ??= new Set(); + this.cloudfrontRegistry[bucketName].add(cloudfrontId); } } diff --git a/test/cross-account-s3.test.ts b/test/cross-account-s3-bucket.test.ts similarity index 100% rename from test/cross-account-s3.test.ts rename to test/cross-account-s3-bucket.test.ts diff --git a/test/cross-account-s3-updater.test.ts b/test/cross-account-s3-updater.test.ts new file mode 100644 index 0000000..cc625b3 --- /dev/null +++ b/test/cross-account-s3-updater.test.ts @@ -0,0 +1,202 @@ +import { App, Stack } from "aws-cdk-lib/core"; +import { Match, Template } from "aws-cdk-lib/assertions"; +import { CrossAccountS3BucketManager } from "../lib"; + +const PYTHON_RUNTIME = "python3.13"; + +const getPoliciesMatcher = (bucketName: string, bucketAwsId: string) => + Match.arrayWith([ + Match.objectLike({ + PolicyDocument: Match.objectLike({ + Statement: Match.arrayWith([ + Match.objectEquals({ + Action: "sts:AssumeRole", + Effect: "Allow", + Resource: `arn:aws:iam::${bucketAwsId}:role/${bucketName}-xa-mgmt`, + }), + ]), + }), + }), + ]); + +const getEventMatcher = (operation: string, distributionIds: string[]) => + Match.objectEquals({ + "Fn::Join": Match.arrayEquals([ + "", + Match.arrayWith([ + Match.stringLikeRegexp( + [operation, "cloudfrontDistributionIds", ...distributionIds].join( + "(.+)", + ), + ), + ]), + ]), + }); + +test("Single XAS3 Manager", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const managerId = "test-xas3-mgr"; + const bucketName = "test-xas3"; + const bucketAwsId = "098765432109"; + + // WHEN + new CrossAccountS3BucketManager(stack, managerId, { + bucketName, + bucketAwsId, + }); + // THEN + const template = Template.fromStack(stack); + + template.hasResourceProperties("AWS::Lambda::Function", { + Runtime: PYTHON_RUNTIME, + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: `${bucketName}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(bucketName, bucketAwsId), + }); + template.hasResourceProperties("Custom::AWS", {}); +}); + +test("XAS3 Manager single accessor", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const managerId = "test-xas3-mgr"; + const bucketName = "test-xas3-s"; + const bucketAwsId = "098765432109"; + const distributionIds = ["SOME_ID"]; + + // WHEN + CrossAccountS3BucketManager.allowCloudfront(bucketName, distributionIds[0]); + new CrossAccountS3BucketManager(stack, managerId, { + bucketName, + bucketAwsId, + }); + // THEN + const template = Template.fromStack(stack); + + template.hasResourceProperties("AWS::Lambda::Function", { + Runtime: PYTHON_RUNTIME, + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: `${bucketName}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(bucketName, bucketAwsId), + }); + template.hasResourceProperties("Custom::AWS", { + Create: getEventMatcher("create", distributionIds), + Update: getEventMatcher("update", distributionIds), + Delete: getEventMatcher("delete", distributionIds), + }); +}); + +test("XAS3 Manager multiple accessors", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const managerId = "test-xas3-mgr"; + const bucketName = "test-xas3-m"; + const bucketAwsId = "098765432109"; + const distributionIds = [ + "SOME_ID", + "ANOTHER_ID", + "YET_ANOTHER_ID", + "SO_MANY_IDS", + ].sort(); + + // WHEN + for (const distributionId of distributionIds) { + CrossAccountS3BucketManager.allowCloudfront(bucketName, distributionId); + } + new CrossAccountS3BucketManager(stack, managerId, { + bucketName, + bucketAwsId, + }); + // THEN + const template = Template.fromStack(stack); + + template.hasResourceProperties("AWS::Lambda::Function", { + Runtime: PYTHON_RUNTIME, + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: `${bucketName}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(bucketName, bucketAwsId), + }); + template.hasResourceProperties("Custom::AWS", { + Create: getEventMatcher("create", distributionIds), + Update: getEventMatcher("update", distributionIds), + Delete: getEventMatcher("delete", distributionIds), + }); +}); + +test("XAS3 Manager improper usage", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const managerId = "test-xas3-mgr"; + const bucketName = "test-xas3-BAD"; + const bucketAwsId = "098765432109"; + + new CrossAccountS3BucketManager(stack, managerId, { + bucketName, + bucketAwsId, + }); + + expect(() => + CrossAccountS3BucketManager.allowCloudfront(bucketName, "bad :("), + ).toThrow(/Cannot register .+ after creation./); +}); + +test("Multiple XAS3 Managers", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const managerId1 = "test-xas3-mgr1"; + const bucketName1 = "test-xas3-s1"; + const bucketAwsId1 = "123456789012"; + const distributionIds1 = ["SOME_ID"]; + const managerId2 = "test-xas3-mgr2"; + const bucketName2 = "test-xas3-m2"; + const bucketAwsId2 = "098765432109"; + const distributionIds2 = [ + "SOME_ID", + "ANOTHER_ID", + "YET_ANOTHER_ID", + "SO_MANY_IDS", + ].sort(); + + // WHEN + CrossAccountS3BucketManager.allowCloudfront(bucketName1, distributionIds1[0]); + for (const distributionId of distributionIds2) { + CrossAccountS3BucketManager.allowCloudfront(bucketName2, distributionId); + } + new CrossAccountS3BucketManager(stack, managerId1, { + bucketName: bucketName1, + bucketAwsId: bucketAwsId1, + }); + new CrossAccountS3BucketManager(stack, managerId2, { + bucketName: bucketName2, + bucketAwsId: bucketAwsId2, + }); + // THEN + const template = Template.fromStack(stack); + + template.hasResourceProperties("AWS::Lambda::Function", { + Runtime: PYTHON_RUNTIME, + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: `${bucketName1}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(bucketName1, bucketAwsId1), + }); + template.hasResourceProperties("Custom::AWS", { + Create: getEventMatcher("create", distributionIds1), + Update: getEventMatcher("update", distributionIds1), + Delete: getEventMatcher("delete", distributionIds1), + }); + + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: `${bucketName2}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(bucketName2, bucketAwsId2), + }); + template.hasResourceProperties("Custom::AWS", { + Create: getEventMatcher("create", distributionIds2), + Update: getEventMatcher("update", distributionIds2), + Delete: getEventMatcher("delete", distributionIds2), + }); +}); From 24bc02a7f7b6c0835ba2440437259af67fe28eec Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sun, 2 Nov 2025 18:09:13 +0900 Subject: [PATCH 06/53] Write s3 policy manager lambda code (enabled testing on LocalStack) --- lib/s3/lambda-code/main.py | 107 +++++++++++++++++++++++++++++++++++-- lib/s3/updater.ts | 40 +++++++++++--- 2 files changed, 138 insertions(+), 9 deletions(-) diff --git a/lib/s3/lambda-code/main.py b/lib/s3/lambda-code/main.py index 3e19b28..50bbda5 100644 --- a/lib/s3/lambda-code/main.py +++ b/lib/s3/lambda-code/main.py @@ -1,13 +1,114 @@ +import json import os import logging import boto3 +from botocore.exceptions import ClientError -s3 = boto3.client("s3") +# Testing +LOCAL = os.environ.get("LOCAL", "false") +local_endpoint_url = "http://localhost:4566" + +# STS client (points to LocalStack if LOCAL environment variable set to true) +sts = ( + boto3.client( + "sts", + endpoint_url=local_endpoint_url, + region_name="ap-northeast-1", + aws_access_key_id="test", + aws_secret_access_key="test", + ) + if LOCAL == "true" + else boto3.client("sts") +) + +XA_MGMT_ROLE_ARN = os.environ["XA_MGMT_ROLE_ARN"] +BUCKET_NAME = os.environ["BUCKET_NAME"] +ACCESSOR_AWS_ID = os.environ["ACCESSOR_AWS_ID"] +ACCESSOR_STACK_NAME = os.environ["ACCESSOR_STACK_NAME"] -# Initialize the logger logger = logging.getLogger() logger.setLevel("INFO") +def s3_client_with_role(operation): + logger.info(f"assuming role: {XA_MGMT_ROLE_ARN}") + assume_role_response = sts.assume_role( + RoleArn=XA_MGMT_ROLE_ARN, + RoleSessionName=f"{BUCKET_NAME}-policy-{operation}", + ) + credentials = assume_role_response["Credentials"] + + session = boto3.Session( + aws_access_key_id=credentials["AccessKeyId"], + aws_secret_access_key=credentials["SecretAccessKey"], + aws_session_token=credentials["SessionToken"], + ) + + return ( + session.client("s3", endpoint_url=local_endpoint_url) + if LOCAL == "true" + else session.client("s3") + ) + + +def get_bucket_policy(s3): + try: + get_policy_response = s3.get_bucket_policy(Bucket=BUCKET_NAME) + return json.loads(get_policy_response["Policy"]) + except ClientError as e: + error_code = e.response.get("Error", {}).get("Code") + if error_code == "NoSuchBucketPolicy": + logger.info(f"No bucket policy found for {BUCKET_NAME}, starting fresh.") + return {"Version": "2012-10-17", "Statement": []} + else: + logger.error(f"Failed to get bucket policy: {e}") + raise + + +def put_bucket_policy(s3, policy): + try: + s3.put_bucket_policy(Bucket=BUCKET_NAME, Policy=json.dumps(policy)) + logger.info("Successfully updated bucket policy") + except ClientError as e: + logger.error(f"Failed to update bucket policy: {e}") + raise + + def handler(event, context): - pass + operation = event["operation"] + logger.info(f"operation: {operation}") + + s3 = s3_client_with_role(operation) + + cloudfront_distribution_ids = sorted(list(event["cloudfrontAccessors"].keys())) + logger.info(f"cloudfrontAccessors: {'\n- '.join(cloudfront_distribution_ids)}") + + # First, remove the statements set by this stack + bucket_policy = get_bucket_policy(s3) + sid_prefix = f"{ACCESSOR_AWS_ID} {ACCESSOR_STACK_NAME} " + bucket_policy["Statement"] = [ + statement + for statement in bucket_policy["Statement"] + if not statement.get("Sid", "").startswith(sid_prefix) + ] + + # Re-add the statements if this is not a delete operation + if operation != "delete": + for id in cloudfront_distribution_ids: + actions = event["cloudfrontAccessors"][id] + statement = { + "Sid": f"{sid_prefix}: cf-{id}", + "Effect": "Allow", + "Principal": {"Service": "cloudfront.amazonaws.com"}, + "Action": actions, + "Resource": f"arn:aws:s3:::{BUCKET_NAME}/*", + "Condition": { + "StringEquals": { + "AWS:SourceArn": f"arn:aws:cloudfront::{ACCESSOR_AWS_ID}:distribution/{id}" + } + }, + } + bucket_policy["Statement"].append(statement) + + # Update the bucket policy + put_bucket_policy(s3, bucket_policy) diff --git a/lib/s3/updater.ts b/lib/s3/updater.ts index 51d9ab1..e11cb99 100644 --- a/lib/s3/updater.ts +++ b/lib/s3/updater.ts @@ -1,4 +1,4 @@ -import { Duration } from "aws-cdk-lib"; +import { Duration, Stack } from "aws-cdk-lib"; import { Effect, PolicyDocument, @@ -16,7 +16,7 @@ import { Construct } from "constructs"; import path from "path"; interface Registry { - [bucketName: string]: Set | false; + [key: string]: Set | false; } export interface CrossAccountS3BucketManagerProps { @@ -46,6 +46,8 @@ export interface CrossAccountS3BucketManagerProps { export class CrossAccountS3BucketManager extends Construct { /** Static registry mapping bucketNames to CloudFront Distribution IDs that need access */ private static cloudfrontRegistry: Registry = {}; + /** Static registry mapping to CloudFront Distribution IDs to S3 permissions */ + private static cloudfrontPermissionsRegistry: Registry = {}; /** * Returns a sorted list of the set of Cloudfront Distribution IDs in the registry and @@ -81,12 +83,13 @@ export class CrossAccountS3BucketManager extends Construct { callerTimeout = 30, // default timeout of 3 seconds is awful short/fragile } = props; + const xaMgmtRoleArn = `arn:aws:iam::${bucketAwsId}:role/${bucketName}-xa-mgmt`; const assumeXaMgmtRole = new PolicyDocument({ statements: [ new PolicyStatement({ effect: Effect.ALLOW, actions: ["sts:AssumeRole"], - resources: [`arn:aws:iam::${bucketAwsId}:role/${bucketName}-xa-mgmt`], + resources: [xaMgmtRoleArn], }), ], }); @@ -95,7 +98,7 @@ export class CrossAccountS3BucketManager extends Construct { assumedBy: new ServicePrincipal("lambda.amazonaws.com"), description: `Execution role for ${bucketName} Lambda function.`, inlinePolicies: { - AssumeXaMgmtRole: assumeXaMgmtRole, + assumeXaMgmtRole, }, roleName: `${bucketName}-xa-mgmt-ex`, }); @@ -105,11 +108,25 @@ export class CrossAccountS3BucketManager extends Construct { handler: "main.handler", runtime: Runtime.PYTHON_3_13, timeout: Duration.seconds(managerTimeout), + environment: { + XA_MGMT_ROLE_ARN: xaMgmtRoleArn, + BUCKET_NAME: bucketName, + ACCESSOR_ACCOUNT_ID: Stack.of(this).account, + ACCESSOR_STACK_NAME: Stack.of(this).stackName, + }, role, }); const cloudfrontDistributionIds = CrossAccountS3BucketManager.consumeCloudfrontAccessors(bucketName); + const cloudfrontAccessors: { [key: string]: string[] } = {}; + for (const id in cloudfrontDistributionIds) { + const actions = + CrossAccountS3BucketManager.cloudfrontPermissionsRegistry[id]; + if (actions) { + cloudfrontAccessors[id] = [...actions].sort(); + } + } const callFor = (operation: string) => { return { @@ -121,7 +138,7 @@ export class CrossAccountS3BucketManager extends Construct { InvocationType: "Event", Payload: JSON.stringify({ operation, - cloudfrontDistributionIds, + cloudfrontAccessors, }), }, }; @@ -138,13 +155,24 @@ export class CrossAccountS3BucketManager extends Construct { }); } - public static allowCloudfront(bucketName: string, cloudfrontId: string) { + /** + * Grant access to the given cross-account S3 bucket to the specified Cloudfront + * Distribution ID + * Optionally specify a list of actions (default: ["s3:GetObject"]) + */ + public static allowCloudfront( + bucketName: string, + cloudfrontId: string, + actions?: string[], + ) { if (this.cloudfrontRegistry[bucketName] === false) { throw new Error( `Cannot register resources for bucket ${bucketName} manager after creation.`, ); } + actions ??= ["s3:GetObject"]; this.cloudfrontRegistry[bucketName] ??= new Set(); this.cloudfrontRegistry[bucketName].add(cloudfrontId); + this.cloudfrontPermissionsRegistry[cloudfrontId] = new Set(actions); } } From dfa6c103e2cca9944846e77cd9b8e2b682bd794f Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sun, 2 Nov 2025 18:41:53 +0900 Subject: [PATCH 07/53] Add KMS module (let's modularize and refactor to reduce copy-pasta and improve maintainability) --- lib/index.ts | 6 ++ lib/kms/index.ts | 5 + lib/kms/key.ts | 80 +++++++++++++++ lib/kms/lambda-code/main.py | 111 +++++++++++++++++++++ lib/kms/updater.ts | 187 ++++++++++++++++++++++++++++++++++++ lib/s3/updater.ts | 4 +- 6 files changed, 391 insertions(+), 2 deletions(-) create mode 100644 lib/kms/key.ts create mode 100644 lib/kms/lambda-code/main.py create mode 100644 lib/kms/updater.ts diff --git a/lib/index.ts b/lib/index.ts index f9c60a1..cc68364 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -3,3 +3,9 @@ export type { CrossAccountS3BucketProps, CrossAccountS3BucketManagerProps, } from "./s3"; + +export { CrossAccountKmsKey, CrossAccountKmsKeyManager } from "./kms"; +export type { + CrossAccountKmsKeyProps, + CrossAccountKmsKeyManagerProps, +} from "./kms"; diff --git a/lib/kms/index.ts b/lib/kms/index.ts index e69de29..d5586e3 100644 --- a/lib/kms/index.ts +++ b/lib/kms/index.ts @@ -0,0 +1,5 @@ +export { CrossAccountKmsKey } from "./key"; +export type { CrossAccountKmsKeyProps } from "./key"; + +export { CrossAccountKmsKeyManager } from "./updater"; +export type { CrossAccountKmsKeyManagerProps } from "./updater"; diff --git a/lib/kms/key.ts b/lib/kms/key.ts new file mode 100644 index 0000000..2e8e8f6 --- /dev/null +++ b/lib/kms/key.ts @@ -0,0 +1,80 @@ +import { Construct } from "constructs"; +import { CfnOutput } from "aws-cdk-lib"; +import { Key, KeyProps } from "aws-cdk-lib/aws-kms"; +import { + AccountRootPrincipal, + ArnPrincipal, + Effect, + PolicyDocument, + PolicyStatement, + Role, +} from "aws-cdk-lib/aws-iam"; + +export interface CrossAccountKmsKeyProps extends KeyProps { + xaAwsIds: string[]; +} + +/** + * CrossAccountKmsKey creates a KMS key with a corresponding IAM role + * that can be assumed by specific cross-account roles to update the resource policy. + * + * @remarks + * - `xaAwsIds` should be the list of AWS account IDs that are allowed to assume + * the management role. + * - The IAM role created is scoped to `kms:GetKeyPolicy` and `kms:PutKeyPolicy` + * on this specific key only. + */ +export class CrossAccountKmsKey extends Construct { + /** The managed KMS key */ + public readonly key: Key; + + /** The IAM role used for cross-account policy management */ + public readonly role: Role; + + constructor(scope: Construct, id: string, props: CrossAccountKmsKeyProps) { + super(scope, id); + + const { xaAwsIds, ...keyProps } = props; + + this.key = new Key(this, "xa-key", keyProps); + + this.role = new Role(this, "xa-mgmt-role", { + assumedBy: new AccountRootPrincipal(), // placeholder + description: `IAM role to enable cross-account management of policy for ${this.key.keyId}`, + roleName: `${this.key.keyId}-xa-mgmt`, + inlinePolicies: { + UpdateKeyPolicy: new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ["kms:GetKeyPolicy", "kms:PutKeyPolicy"], + resources: [this.key.keyArn], + }), + ], + }), + }, + }); + + const roleName = `${this.key.keyId}-xa-mgmt-ex`; + this.role.assumeRolePolicy?.addStatements( + new PolicyStatement({ + effect: Effect.ALLOW, + principals: xaAwsIds.map( + (id) => new ArnPrincipal(`arn:aws:iam::${id}:role/${roleName}`), + ), + actions: ["sts:AssumeRole"], + }), + ); + + new CfnOutput(this, "key-id", { + value: this.key.keyId, + description: "ID of the cross-account managed KMS key", + }); + + new CfnOutput(this, "xa-mgmt-role-arn", { + value: this.role.roleArn, + description: + "ARN of the IAM role used for cross-account key policy management", + }); + } +} diff --git a/lib/kms/lambda-code/main.py b/lib/kms/lambda-code/main.py new file mode 100644 index 0000000..2f9a60a --- /dev/null +++ b/lib/kms/lambda-code/main.py @@ -0,0 +1,111 @@ +import json +import os +import logging +import boto3 +from botocore.exceptions import ClientError + +# Testing +LOCAL = os.environ.get("LOCAL", "false") +local_endpoint_url = "http://localhost:4566" + +# STS client (points to LocalStack if LOCAL environment variable set to true) +sts = ( + boto3.client( + "sts", + endpoint_url=local_endpoint_url, + region_name="ap-northeast-1", + aws_access_key_id="test", + aws_secret_access_key="test", + ) + if LOCAL == "true" + else boto3.client("sts") +) + +XA_MGMT_ROLE_ARN = os.environ["XA_MGMT_ROLE_ARN"] +KEY_ID = os.environ["KEY_ID"] +ACCESSOR_AWS_ID = os.environ["ACCESSOR_AWS_ID"] +ACCESSOR_STACK_NAME = os.environ["ACCESSOR_STACK_NAME"] + +logger = logging.getLogger() +logger.setLevel("INFO") + + +def kms_client_with_role(operation): + logger.info(f"assuming role: {XA_MGMT_ROLE_ARN}") + assume_role_response = sts.assume_role( + RoleArn=XA_MGMT_ROLE_ARN, + RoleSessionName=f"{KEY_ID}-policy-{operation}", + ) + credentials = assume_role_response["Credentials"] + + session = boto3.Session( + aws_access_key_id=credentials["AccessKeyId"], + aws_secret_access_key=credentials["SecretAccessKey"], + aws_session_token=credentials["SessionToken"], + ) + + return ( + session.client("kms", endpoint_url=local_endpoint_url) + if LOCAL == "true" + else session.client("kms") + ) + + +def get_key_policy(kms): + try: + get_policy_response = kms.get_key_policy(KeyId=KEY_ID, PolicyName="default") + return json.loads(get_policy_response["Policy"]) + except ClientError as e: + logger.error(f"Failed to get key policy: {e}") + raise + + +def put_key_policy(kms, policy): + try: + kms.put_key_policy( + KeyId=KEY_ID, PolicyName="default", Policy=json.dumps(policy) + ) + logger.info("Successfully updated key policy") + except ClientError as e: + logger.error(f"Failed to update key policy: {e}") + raise + + +def handler(event, context): + operation = event["operation"] + logger.info(f"operation: {operation}") + + kms = kms_client_with_role(operation) + + cloudfront_distribution_ids = sorted(list(event["cloudfrontAccessors"].keys())) + logger.info(f"cloudfrontAccessors: {'\n- '.join(cloudfront_distribution_ids)}") + + # First, remove the statements set by this stack + key_policy = get_key_policy(kms) + sid_prefix = f"{ACCESSOR_AWS_ID} {ACCESSOR_STACK_NAME} " + key_policy["Statement"] = [ + statement + for statement in key_policy["Statement"] + if not statement.get("Sid", "").startswith(sid_prefix) + ] + + # Re-add the statements if this is not a delete operation + if operation != "delete": + for id in cloudfront_distribution_ids: + actions = event["cloudfrontAccessors"][id] + statement = { + "Sid": f"{sid_prefix}: cf-{id}", + "Effect": "Allow", + "Principal": {"Service": "cloudfront.amazonaws.com"}, + "Action": actions, + "Resource": "*", + "Condition": { + "StringEquals": { + "AWS:SourceArn": f"arn:aws:cloudfront::{ACCESSOR_AWS_ID}:distribution/{id}" + } + }, + } + key_policy["Statement"].append(statement) + + # Update the key policy + put_key_policy(kms, key_policy) diff --git a/lib/kms/updater.ts b/lib/kms/updater.ts new file mode 100644 index 0000000..b5f367d --- /dev/null +++ b/lib/kms/updater.ts @@ -0,0 +1,187 @@ +import { Duration, Stack } from "aws-cdk-lib"; +import { + Effect, + PolicyDocument, + PolicyStatement, + Role, + ServicePrincipal, +} from "aws-cdk-lib/aws-iam"; +import { Code, Function, Runtime } from "aws-cdk-lib/aws-lambda"; +import { + AwsCustomResource, + AwsCustomResourcePolicy, + PhysicalResourceId, +} from "aws-cdk-lib/custom-resources"; +import { Construct } from "constructs"; +import path from "path"; + +interface Registry { + [key: string]: Set | false; +} + +export interface CrossAccountKmsKeyManagerProps { + keyId: string; + keyAwsId: string; + managerTimeout?: number; + callerTimeout?: number; +} + +/** + * CrossAccountKmsKeyManager creates a Lambda function and calls it at deployment + * time using an AwsCustomResource (Lambda:InvokeFunction). + * + * @remarks + * - `keyId` should be the ID of the KMS key in the other account whose + * policy we want to manage. + * - `keyAwsId` should be the ID of the AWS account the aforementioned key + * belongs to. + * - `managerTimeout` is optional and specifies the number of seconds for the manager + * Lambda Function's timeout (defaults to 30). + * - `callerTimeout` is optional and specifies the number of seconds for the + * AwsCustomResource's timeout (defaults to 30). + * - To use this construct, first register the resources that need to access a given + * KMS key using one of the following static registry functions: + * - allowCloudfront(keyId, cloudfrontId) + */ +export class CrossAccountKmsKeyManager extends Construct { + /** Static registry mapping keyIds to CloudFront Distribution IDs that need access */ + private static cloudfrontRegistry: Registry = {}; + /** Static registry mapping to CloudFront Distribution IDs to KMS permissions */ + private static cloudfrontPermissionsRegistry: Registry = {}; + + /** + * Returns a sorted list of the set of Cloudfront Distribution IDs in the registry and + * sets the manager to created + */ + private static consumeCloudfrontAccessors(keyId: string): string[] { + const accessors = [ + ...(this.cloudfrontRegistry[keyId] ? this.cloudfrontRegistry[keyId] : []), + ].sort(); + this.cloudfrontRegistry[keyId] = false; + return accessors; + } + + /** + * The Lambda Function that will be called to assume a role cross-account and + * manage the KMS Key + */ + public readonly function: Function; + + constructor( + scope: Construct, + id: string, + props: CrossAccountKmsKeyManagerProps, + ) { + super(scope, id); + + const { + keyId, + keyAwsId, + managerTimeout = 30, // default timeout of 3 seconds is awful short/fragile + callerTimeout = 30, // default timeout of 3 seconds is awful short/fragile + } = props; + + const xaMgmtRoleArn = `arn:aws:iam::${keyAwsId}:role/${keyId}-xa-mgmt`; + const assumeXaMgmtRole = new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ["sts:AssumeRole"], + resources: [xaMgmtRoleArn], + }), + ], + }); + + const role = new Role(this, "xa-mgmt-lambda-role", { + assumedBy: new ServicePrincipal("lambda.amazonaws.com"), + description: `Execution role for ${keyId} manager Lambda function.`, + inlinePolicies: { + assumeXaMgmtRole, + }, + roleName: `${keyId}-xa-mgmt-ex`, + }); + + this.function = new Function(this, "xa-mgmt-lambda", { + code: Code.fromAsset(path.join(__dirname, "lambda-code")), + handler: "main.handler", + runtime: Runtime.PYTHON_3_13, + timeout: Duration.seconds(managerTimeout), + environment: { + XA_MGMT_ROLE_ARN: xaMgmtRoleArn, + KEY_ID: keyId, + ACCESSOR_ACCOUNT_ID: Stack.of(this).account, + ACCESSOR_STACK_NAME: Stack.of(this).stackName, + }, + role, + }); + + const cloudfrontDistributionIds = + CrossAccountKmsKeyManager.consumeCloudfrontAccessors(keyId); + const cloudfrontAccessors: { [key: string]: string[] } = {}; + for (const id in cloudfrontDistributionIds) { + const actions = + CrossAccountKmsKeyManager.cloudfrontPermissionsRegistry[id]; + if (actions) { + cloudfrontAccessors[id] = [...actions].sort(); + } + } + + const callFor = (operation: string) => { + return { + physicalResourceId: PhysicalResourceId.of("xa-mgmt-lambda-caller"), + service: "Lambda", + action: "InvokeFunction", + parameters: { + FunctionName: this.function.functionName, + InvocationType: "Event", + Payload: JSON.stringify({ + operation, + cloudfrontAccessors, + }), + }, + }; + }; + + new AwsCustomResource(this, "xa-mgmt-lambda-caller", { + onCreate: callFor("create"), + onUpdate: callFor("update"), + onDelete: callFor("delete"), + policy: AwsCustomResourcePolicy.fromSdkCalls({ + resources: [this.function.functionArn], + }), + timeout: Duration.seconds(callerTimeout), + }); + } + + /** + * Grant access to the given cross-account KMS key to the specified Cloudfront + * Distribution ID + * Optionally specify a list of actions + * (default: [ + * "kms:Decrypt", + * "kms:Encrypt", + * "kms:GenerateDataKey*", + * "kms:DescribeKey" + * ]) + */ + public static allowCloudfront( + keyId: string, + cloudfrontId: string, + actions?: string[], + ) { + if (this.cloudfrontRegistry[keyId] === false) { + throw new Error( + `Cannot register resources for key ${keyId} manager after creation.`, + ); + } + actions ??= [ + "kms:Decrypt", + "kms:Encrypt", + "kms:GenerateDataKey*", + "kms:DescribeKey", + ]; + this.cloudfrontRegistry[keyId] ??= new Set(); + this.cloudfrontRegistry[keyId].add(cloudfrontId); + this.cloudfrontPermissionsRegistry[cloudfrontId] = new Set(actions); + } +} diff --git a/lib/s3/updater.ts b/lib/s3/updater.ts index e11cb99..d8e7e86 100644 --- a/lib/s3/updater.ts +++ b/lib/s3/updater.ts @@ -96,7 +96,7 @@ export class CrossAccountS3BucketManager extends Construct { const role = new Role(this, "xa-mgmt-lambda-role", { assumedBy: new ServicePrincipal("lambda.amazonaws.com"), - description: `Execution role for ${bucketName} Lambda function.`, + description: `Execution role for ${bucketName} manager Lambda function.`, inlinePolicies: { assumeXaMgmtRole, }, @@ -144,7 +144,7 @@ export class CrossAccountS3BucketManager extends Construct { }; }; - const caller = new AwsCustomResource(this, "xa-mgmt-lambda-caller", { + new AwsCustomResource(this, "xa-mgmt-lambda-caller", { onCreate: callFor("create"), onUpdate: callFor("update"), onDelete: callFor("delete"), From ca8cc0567b8da1c5c612358df0d73867c5289101 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sun, 2 Nov 2025 18:49:35 +0900 Subject: [PATCH 08/53] Fix permissions access and s3 tests --- lib/kms/updater.ts | 2 +- lib/s3/updater.ts | 2 +- test/cross-account-s3-updater.test.ts | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/kms/updater.ts b/lib/kms/updater.ts index b5f367d..5b1d86a 100644 --- a/lib/kms/updater.ts +++ b/lib/kms/updater.ts @@ -118,7 +118,7 @@ export class CrossAccountKmsKeyManager extends Construct { const cloudfrontDistributionIds = CrossAccountKmsKeyManager.consumeCloudfrontAccessors(keyId); const cloudfrontAccessors: { [key: string]: string[] } = {}; - for (const id in cloudfrontDistributionIds) { + for (const id of cloudfrontDistributionIds) { const actions = CrossAccountKmsKeyManager.cloudfrontPermissionsRegistry[id]; if (actions) { diff --git a/lib/s3/updater.ts b/lib/s3/updater.ts index d8e7e86..42d2cf6 100644 --- a/lib/s3/updater.ts +++ b/lib/s3/updater.ts @@ -120,7 +120,7 @@ export class CrossAccountS3BucketManager extends Construct { const cloudfrontDistributionIds = CrossAccountS3BucketManager.consumeCloudfrontAccessors(bucketName); const cloudfrontAccessors: { [key: string]: string[] } = {}; - for (const id in cloudfrontDistributionIds) { + for (const id of cloudfrontDistributionIds) { const actions = CrossAccountS3BucketManager.cloudfrontPermissionsRegistry[id]; if (actions) { diff --git a/test/cross-account-s3-updater.test.ts b/test/cross-account-s3-updater.test.ts index cc625b3..42ac552 100644 --- a/test/cross-account-s3-updater.test.ts +++ b/test/cross-account-s3-updater.test.ts @@ -25,9 +25,7 @@ const getEventMatcher = (operation: string, distributionIds: string[]) => "", Match.arrayWith([ Match.stringLikeRegexp( - [operation, "cloudfrontDistributionIds", ...distributionIds].join( - "(.+)", - ), + [operation, "cloudfrontAccessors", ...distributionIds].join("(.+)"), ), ]), ]), From 898187379fc1ab9ad7bd8ce8ed7e46463c0a52c4 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sun, 2 Nov 2025 20:49:39 +0900 Subject: [PATCH 09/53] Move common xa access role creation to parent class --- lib/cross-account/index.ts | 2 + lib/cross-account/xa-construct.ts | 106 ++++++++++++++++++++++++++++++ lib/kms/key.ts | 61 ++++------------- lib/s3/bucket.ts | 61 ++++------------- 4 files changed, 132 insertions(+), 98 deletions(-) create mode 100644 lib/cross-account/index.ts create mode 100644 lib/cross-account/xa-construct.ts diff --git a/lib/cross-account/index.ts b/lib/cross-account/index.ts new file mode 100644 index 0000000..60b24da --- /dev/null +++ b/lib/cross-account/index.ts @@ -0,0 +1,2 @@ +export { CrossAccountConstruct } from "./xa-construct"; +export type { CrossAccountConstructProps } from "./xa-construct"; diff --git a/lib/cross-account/xa-construct.ts b/lib/cross-account/xa-construct.ts new file mode 100644 index 0000000..7afe083 --- /dev/null +++ b/lib/cross-account/xa-construct.ts @@ -0,0 +1,106 @@ +import { Construct } from "constructs"; +import { CfnOutput } from "aws-cdk-lib"; +import { + AccountRootPrincipal, + ArnPrincipal, + Effect, + PolicyDocument, + PolicyStatement, + Role, +} from "aws-cdk-lib/aws-iam"; + +/** + * Properties for CrossAccountConstruct. + */ +export interface CrossAccountConstructProps { + /** + * List of AWS account IDs allowed to assume the management role. + * Each string should be a 12-digit numeric AWS account ID. + */ + xaAwsIds: string[]; +} + +/** + * Abstract base class for cross-account resource management. + * + * @remarks + * Provides a standard way to create an IAM management role that can be + * assumed by specified accounts to manage resource policies (e.g., S3 bucket policies, KMS key policies). + * + * Subclasses should call `createManagementRole()` in their constructor or initialization + * to set up the IAM role and grant cross-account assume-role permissions. + */ +export abstract class CrossAccountConstruct extends Construct { + /** The IAM role used for cross-account policy management */ + private mgmtRole: Role; + + /** Getter for the cross-account management role */ + public get role(): Role { + return this.mgmtRole; + } + + private readonly xaAwsIds: string[]; + + /** + * Creates the IAM role used to manage policies on the target resource. + * + * @param resourceIdentifier - A human-readable identifier for the resource (used in the role name and description). + * @param policyTarget - The resource target in the policy for this role (usually an ARN). + * @param policyActions - List of actions the role can perform on the resource (e.g., ["s3:GetBucketPolicy", "s3:PutBucketPolicy"]). + */ + protected createManagementRole( + resourceIdentifier: string, + policyTarget: string, + policyActions: string[], + ) { + this.mgmtRole = new Role(this, "xa-mgmt-role", { + assumedBy: new AccountRootPrincipal(), // placeholder + description: `IAM role to enable cross-account management of policy for ${resourceIdentifier}`, + roleName: `${resourceIdentifier}-xa-mgmt`, + inlinePolicies: { + UpdateResourcePolicy: new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: policyActions, + resources: [policyTarget], + }), + ], + }), + }, + }); + const accessorRoleName = `${resourceIdentifier}-xa-mgmt-ex`; + + this.role.assumeRolePolicy?.addStatements( + new PolicyStatement({ + effect: Effect.ALLOW, + principals: this.xaAwsIds.map( + (id) => + new ArnPrincipal(`arn:aws:iam::${id}:role/${accessorRoleName}`), + ), + actions: ["sts:AssumeRole"], + }), + ); + + new CfnOutput(this, "xa-mgmt-role-arn", { + value: this.role.roleArn, + description: + "ARN of the IAM role used for cross-account resource policy management", + }); + } + + /** + * Constructs a CrossAccountConstruct. + * + * @param scope - The parent Construct scope. + * @param id - Logical ID of the construct. + * @param props - Properties including the cross-account AWS IDs. + */ + constructor(scope: Construct, id: string, props: CrossAccountConstructProps) { + super(scope, id); + + const { xaAwsIds } = props; + + this.xaAwsIds = xaAwsIds; + } +} diff --git a/lib/kms/key.ts b/lib/kms/key.ts index 2e8e8f6..eae1a8a 100644 --- a/lib/kms/key.ts +++ b/lib/kms/key.ts @@ -2,17 +2,13 @@ import { Construct } from "constructs"; import { CfnOutput } from "aws-cdk-lib"; import { Key, KeyProps } from "aws-cdk-lib/aws-kms"; import { - AccountRootPrincipal, - ArnPrincipal, - Effect, - PolicyDocument, - PolicyStatement, - Role, -} from "aws-cdk-lib/aws-iam"; + CrossAccountConstruct, + CrossAccountConstructProps, +} from "../cross-account"; -export interface CrossAccountKmsKeyProps extends KeyProps { - xaAwsIds: string[]; -} +export interface CrossAccountKmsKeyProps + extends KeyProps, + CrossAccountConstructProps {} /** * CrossAccountKmsKey creates a KMS key with a corresponding IAM role @@ -24,57 +20,24 @@ export interface CrossAccountKmsKeyProps extends KeyProps { * - The IAM role created is scoped to `kms:GetKeyPolicy` and `kms:PutKeyPolicy` * on this specific key only. */ -export class CrossAccountKmsKey extends Construct { +export class CrossAccountKmsKey extends CrossAccountConstruct { /** The managed KMS key */ public readonly key: Key; - /** The IAM role used for cross-account policy management */ - public readonly role: Role; - constructor(scope: Construct, id: string, props: CrossAccountKmsKeyProps) { - super(scope, id); - const { xaAwsIds, ...keyProps } = props; + super(scope, id, { xaAwsIds }); this.key = new Key(this, "xa-key", keyProps); - this.role = new Role(this, "xa-mgmt-role", { - assumedBy: new AccountRootPrincipal(), // placeholder - description: `IAM role to enable cross-account management of policy for ${this.key.keyId}`, - roleName: `${this.key.keyId}-xa-mgmt`, - inlinePolicies: { - UpdateKeyPolicy: new PolicyDocument({ - statements: [ - new PolicyStatement({ - effect: Effect.ALLOW, - actions: ["kms:GetKeyPolicy", "kms:PutKeyPolicy"], - resources: [this.key.keyArn], - }), - ], - }), - }, - }); - - const roleName = `${this.key.keyId}-xa-mgmt-ex`; - this.role.assumeRolePolicy?.addStatements( - new PolicyStatement({ - effect: Effect.ALLOW, - principals: xaAwsIds.map( - (id) => new ArnPrincipal(`arn:aws:iam::${id}:role/${roleName}`), - ), - actions: ["sts:AssumeRole"], - }), - ); + this.createManagementRole(this.key.keyId, this.key.keyArn, [ + "kms:GetKeyPolicy", + "kms:PutKeyPolicy", + ]); new CfnOutput(this, "key-id", { value: this.key.keyId, description: "ID of the cross-account managed KMS key", }); - - new CfnOutput(this, "xa-mgmt-role-arn", { - value: this.role.roleArn, - description: - "ARN of the IAM role used for cross-account key policy management", - }); } } diff --git a/lib/s3/bucket.ts b/lib/s3/bucket.ts index fe89116..46bd670 100644 --- a/lib/s3/bucket.ts +++ b/lib/s3/bucket.ts @@ -2,17 +2,13 @@ import { Construct } from "constructs"; import { CfnOutput } from "aws-cdk-lib"; import { Bucket, BucketProps } from "aws-cdk-lib/aws-s3"; import { - AccountRootPrincipal, - ArnPrincipal, - Effect, - PolicyDocument, - PolicyStatement, - Role, -} from "aws-cdk-lib/aws-iam"; + CrossAccountConstruct, + CrossAccountConstructProps, +} from "../cross-account"; -export interface CrossAccountS3BucketProps extends BucketProps { - xaAwsIds: string[]; -} +export interface CrossAccountS3BucketProps + extends BucketProps, + CrossAccountConstructProps {} /** * CrossAccountS3Bucket creates an S3 bucket with a corresponding IAM role @@ -24,17 +20,13 @@ export interface CrossAccountS3BucketProps extends BucketProps { * - The IAM role created is scoped to `s3:GetBucketPolicy` and `s3:PutBucketPolicy` * on the bucket only. */ -export class CrossAccountS3Bucket extends Construct { +export class CrossAccountS3Bucket extends CrossAccountConstruct { /** The managed S3 bucket */ public readonly bucket: Bucket; - /** The IAM role used for cross-account policy management */ - public readonly role: Role; - constructor(scope: Construct, id: string, props: CrossAccountS3BucketProps) { - super(scope, id); - const { xaAwsIds, ...bucketProps } = props; + super(scope, id, { xaAwsIds }); // The updater Lambda's execution role will have 11 characters appended to it // so let's keep the bucket name to 52 characters or less @@ -46,43 +38,14 @@ export class CrossAccountS3Bucket extends Construct { this.bucket = new Bucket(this, "xa-bucket", bucketProps); - this.role = new Role(this, "xa-mgmt-role", { - assumedBy: new AccountRootPrincipal(), // placeholder - description: `IAM role to enable cross-account management of policy for ${this.bucket.bucketName}`, - roleName: `${this.bucket.bucketName}-xa-mgmt`, - inlinePolicies: { - UpdateBucketPolicy: new PolicyDocument({ - statements: [ - new PolicyStatement({ - effect: Effect.ALLOW, - actions: ["s3:GetBucketPolicy", "s3:PutBucketPolicy"], - resources: [this.bucket.bucketArn], - }), - ], - }), - }, - }); - - const roleName = `${this.bucket.bucketName}-xa-mgmt-ex`; - this.role.assumeRolePolicy?.addStatements( - new PolicyStatement({ - effect: Effect.ALLOW, - principals: xaAwsIds.map( - (id) => new ArnPrincipal(`arn:aws:iam::${id}:role/${roleName}`), - ), - actions: ["sts:AssumeRole"], - }), - ); + this.createManagementRole(this.bucket.bucketName, this.bucket.bucketArn, [ + "s3:GetBucketPolicy", + "s3:PutBucketPolicy", + ]); new CfnOutput(this, "bucket-name", { value: this.bucket.bucketName, description: "Name of the cross-account managed S3 bucket", }); - - new CfnOutput(this, "xa-mgmt-role-arn", { - value: this.role.roleArn, - description: - "ARN of the IAM role used for cross-account bucket policy management", - }); } } From a44ba8a381c65a8af6a5b97aefcc22ab33417ee8 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Wed, 5 Nov 2025 08:44:36 +0900 Subject: [PATCH 10/53] Move xa-manager code to abstract parent, implement in S3 manager --- lib/cross-account/index.ts | 3 + lib/cross-account/xa-manager-registry.ts | 91 ++++++++++++ lib/cross-account/xa-manager.ts | 169 +++++++++++++++++++++++ lib/s3/updater.ts | 137 +++--------------- 4 files changed, 286 insertions(+), 114 deletions(-) create mode 100644 lib/cross-account/xa-manager-registry.ts create mode 100644 lib/cross-account/xa-manager.ts diff --git a/lib/cross-account/index.ts b/lib/cross-account/index.ts index 60b24da..e70b5da 100644 --- a/lib/cross-account/index.ts +++ b/lib/cross-account/index.ts @@ -1,2 +1,5 @@ export { CrossAccountConstruct } from "./xa-construct"; export type { CrossAccountConstructProps } from "./xa-construct"; + +export { CrossAccountManager } from "./xa-manager"; +export type { CrossAccountManagerProps } from "./xa-manager"; diff --git a/lib/cross-account/xa-manager-registry.ts b/lib/cross-account/xa-manager-registry.ts new file mode 100644 index 0000000..8900a2c --- /dev/null +++ b/lib/cross-account/xa-manager-registry.ts @@ -0,0 +1,91 @@ +// Sveltekit-style state manager + +import { Stack } from "aws-cdk-lib"; + +interface Access { + accessorIdentifier: string; + targetIdentifier: string; + permissions: string[]; +} +interface ConsumptionStatus { + targetIdentifier: string; + consumed: boolean; +} + +// A map of Function (manager Construct constuctor) -> cloudfrontAccessors array (set to false if consumed) +type Registry = Map; +const ensureRegistry = ( + registry: WeakMap>, + stack: Stack, + manager: Function, +) => { + if (!registry.has(stack)) registry.set(stack, new Map()); + const stackRegistry = registry.get(stack)!; + if (!stackRegistry.has(manager)) stackRegistry.set(manager, []); + return stackRegistry.get(manager)!; +}; + +// A map of Stack instance -> Function (manager Construct constuctor) -> cloudfrontAccessors array +const cloudfrontRegistries = new WeakMap>(); + +// A map of Stack instance -> Function (manager Construct constuctor) -> ConsumptionStatus array +const consumptionRegistries = new WeakMap>(); + +// Checks if the given resource manager for the given stack has been consumed +const isConsumed = ( + stack: Stack, + manager: Function, + targetIdentifier: string, +) => + consumptionRegistries + .get(stack) + ?.get(manager) + ?.find((r) => r.targetIdentifier == targetIdentifier)?.consumed ?? false; + +// Consume the cloudfrontAccessors for the provided manager construct of the given stack +export const consumeCloudfrontAccessors = ( + stack: Stack, + manager: Function, + targetIdentifier: string, +) => { + if (isConsumed(stack, manager, targetIdentifier)) + throw new Error( + `Manager for ${targetIdentifier} has already been consumed.`, + ); + ensureRegistry(consumptionRegistries, stack, manager).push({ + targetIdentifier, + consumed: true, + }); + return ensureRegistry(cloudfrontRegistries, stack, manager).filter( + (r) => r.targetIdentifier, + ); +}; + +// Manager functions that call this should specify a default list of actions for ergonomics +export const addCloudfrontAccessor = ( + stack: Stack, + manager: Function, + distributionId: string, + targetIdentifier: string, + actions: string[], +) => { + if (isConsumed(stack, manager, targetIdentifier)) + throw new Error( + `Cannot register resources for ${targetIdentifier} manager after creation` + + `(registering ${distributionId}).`, + ); + if ( + ensureRegistry(cloudfrontRegistries, stack, manager).find( + (r) => r.accessorIdentifier == distributionId, + ) + ) + throw new Error( + `Distribution ${distributionId} has already been registered for` + + `${targetIdentifier} manager.`, + ); + ensureRegistry(cloudfrontRegistries, stack, manager).push({ + accessorIdentifier: distributionId, + targetIdentifier, + permissions: actions, + }); +}; diff --git a/lib/cross-account/xa-manager.ts b/lib/cross-account/xa-manager.ts new file mode 100644 index 0000000..4499c25 --- /dev/null +++ b/lib/cross-account/xa-manager.ts @@ -0,0 +1,169 @@ +import { Duration, Stack } from "aws-cdk-lib"; +import { + Effect, + PolicyDocument, + PolicyStatement, + Role, + ServicePrincipal, +} from "aws-cdk-lib/aws-iam"; +import { + Code, + Function as LambdaFunction, + Runtime, +} from "aws-cdk-lib/aws-lambda"; +import { + AwsCustomResource, + AwsCustomResourcePolicy, + PhysicalResourceId, +} from "aws-cdk-lib/custom-resources"; +import { Construct } from "constructs"; +import path from "path"; +import { + addCloudfrontAccessor, + consumeCloudfrontAccessors, +} from "./xa-manager-registry"; + +export interface CrossAccountManagerProps { + resourceIdentifier: string; + xaAwsId: string; + managerTimeout?: number; + callerTimeout?: number; +} + +export abstract class CrossAccountManager extends Construct { + /** + * The Lambda Function that will be called to assume a role cross-account and + * manage the resource + */ + private mgrFunction: LambdaFunction; + public get function(): LambdaFunction { + return this.mgrFunction; + } + + /** + * Given a stack, subclass constructor, and a resource identifier, returns + * the mapping of accessor resource IDs to the permissions they need. + */ + private static consumeCloudfrontAccessors( + stack: Stack, + caller: Function, + resourceIdentifier: string, + ) { + const accessInfo = consumeCloudfrontAccessors( + stack, + caller, + resourceIdentifier, + ); + const accessors: { [accessor: string]: string[] } = {}; + for (const access of accessInfo) { + accessors[access.accessorIdentifier] = access.permissions; + } + return accessors; + } + + constructor(scope: Construct, id: string, props: CrossAccountManagerProps) { + super(scope, id); + + const { + resourceIdentifier, + xaAwsId, + managerTimeout = 30, // default timeout of 3 seconds is awful short/fragile + callerTimeout = 30, // default timeout of 3 seconds is awful short/fragile + } = props; + + const xaMgmtRoleArn = `arn:aws:iam::${xaAwsId}:role/${resourceIdentifier}-xa-mgmt`; + const assumeXaMgmtRolePolicy = new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ["sts:AssumeRole"], + resources: [xaMgmtRoleArn], + }), + ], + }); + + // Manager Lambda execution role + const role = new Role(this, "xa-mgmt-lambda-role", { + assumedBy: new ServicePrincipal("lambda.amazonaws.com"), + description: `Execution role for ${resourceIdentifier} manager Lambda function.`, + inlinePolicies: { + assumeXaMgmtRolePolicy, + }, + roleName: `${resourceIdentifier}-xa-mgmt-ex`, + }); + + // Manager Lambda + this.mgrFunction = new LambdaFunction(this, "xa-mgmt-lambda", { + code: Code.fromAsset(path.join(__dirname, "lambda-code")), + handler: "main.handler", + runtime: Runtime.PYTHON_3_13, + timeout: Duration.seconds(managerTimeout), + environment: { + XA_MGMT_ROLE_ARN: xaMgmtRoleArn, + RESOURCE_ID: resourceIdentifier, + ACCESSOR_ACCOUNT_ID: Stack.of(this).account, + ACCESSOR_STACK_NAME: Stack.of(this).stackName, + }, + role, + }); + + // Get the Cloudfront IDs and their permissions for this subclass from the + // Registries + const stack = Stack.of(this); + const caller = this.constructor; + const cloudfrontAccessors = CrossAccountManager.consumeCloudfrontAccessors( + stack, + caller, + resourceIdentifier, + ); + + // Util factory to get an AwsSdkCall for the AwsCustomResource + const callFor = (operation: string) => { + return { + physicalResourceId: PhysicalResourceId.of("xa-mgmt-lambda-caller"), + service: "Lambda", + action: "InvokeFunction", + parameters: { + FunctionName: this.function.functionName, + InvocationType: "Event", + Payload: JSON.stringify({ + operation, + cloudfrontAccessors, + }), + }, + }; + }; + + new AwsCustomResource(this, "xa-mgmt-lambda-caller", { + onCreate: callFor("create"), + onUpdate: callFor("update"), + onDelete: callFor("delete"), + policy: AwsCustomResourcePolicy.fromSdkCalls({ + resources: [this.function.functionArn], + }), + timeout: Duration.seconds(callerTimeout), + }); + } + + /** + * Grant access to the given cross-account resource to the specified Cloudfront + * Distribution ID + * Should be implemented by the child class, passing the subclass's constructor and + * specifying a default list of actions + */ + protected static registerCloudfrontAccessor( + stack: Stack, + caller: Function, + resourceIdentifier: string, + cloudfrontId: string, + actions: string[], + ) { + addCloudfrontAccessor( + stack, + caller, + resourceIdentifier, + cloudfrontId, + actions, + ); + } +} diff --git a/lib/s3/updater.ts b/lib/s3/updater.ts index 42d2cf6..8a8e05e 100644 --- a/lib/s3/updater.ts +++ b/lib/s3/updater.ts @@ -14,14 +14,18 @@ import { } from "aws-cdk-lib/custom-resources"; import { Construct } from "constructs"; import path from "path"; +import { + CrossAccountManager, + CrossAccountManagerProps, +} from "../cross-account"; interface Registry { [key: string]: Set | false; } export interface CrossAccountS3BucketManagerProps { - bucketName: string; - bucketAwsId: string; + xaBucketName: string; + xaAwsId: string; managerTimeout?: number; callerTimeout?: number; } @@ -43,115 +47,18 @@ export interface CrossAccountS3BucketManagerProps { * S3 Bucket using one of the following static registry functions: * - allowCloudfront(bucketName, cloudfrontId) */ -export class CrossAccountS3BucketManager extends Construct { - /** Static registry mapping bucketNames to CloudFront Distribution IDs that need access */ - private static cloudfrontRegistry: Registry = {}; - /** Static registry mapping to CloudFront Distribution IDs to S3 permissions */ - private static cloudfrontPermissionsRegistry: Registry = {}; - - /** - * Returns a sorted list of the set of Cloudfront Distribution IDs in the registry and - * sets the manager to created - */ - private static consumeCloudfrontAccessors(bucketName: string): string[] { - const accessors = [ - ...(this.cloudfrontRegistry[bucketName] - ? this.cloudfrontRegistry[bucketName] - : []), - ].sort(); - this.cloudfrontRegistry[bucketName] = false; - return accessors; - } - - /** - * The Lambda Function that will be called to assume a role cross-account and - * manage the S3 Bucket - */ - public readonly function: Function; - +export class CrossAccountS3BucketManager extends CrossAccountManager { constructor( scope: Construct, id: string, props: CrossAccountS3BucketManagerProps, ) { - super(scope, id); - - const { - bucketName, - bucketAwsId, - managerTimeout = 30, // default timeout of 3 seconds is awful short/fragile - callerTimeout = 30, // default timeout of 3 seconds is awful short/fragile - } = props; - - const xaMgmtRoleArn = `arn:aws:iam::${bucketAwsId}:role/${bucketName}-xa-mgmt`; - const assumeXaMgmtRole = new PolicyDocument({ - statements: [ - new PolicyStatement({ - effect: Effect.ALLOW, - actions: ["sts:AssumeRole"], - resources: [xaMgmtRoleArn], - }), - ], - }); - - const role = new Role(this, "xa-mgmt-lambda-role", { - assumedBy: new ServicePrincipal("lambda.amazonaws.com"), - description: `Execution role for ${bucketName} manager Lambda function.`, - inlinePolicies: { - assumeXaMgmtRole, - }, - roleName: `${bucketName}-xa-mgmt-ex`, - }); - - this.function = new Function(this, "xa-mgmt-lambda", { - code: Code.fromAsset(path.join(__dirname, "lambda-code")), - handler: "main.handler", - runtime: Runtime.PYTHON_3_13, - timeout: Duration.seconds(managerTimeout), - environment: { - XA_MGMT_ROLE_ARN: xaMgmtRoleArn, - BUCKET_NAME: bucketName, - ACCESSOR_ACCOUNT_ID: Stack.of(this).account, - ACCESSOR_STACK_NAME: Stack.of(this).stackName, - }, - role, - }); - - const cloudfrontDistributionIds = - CrossAccountS3BucketManager.consumeCloudfrontAccessors(bucketName); - const cloudfrontAccessors: { [key: string]: string[] } = {}; - for (const id of cloudfrontDistributionIds) { - const actions = - CrossAccountS3BucketManager.cloudfrontPermissionsRegistry[id]; - if (actions) { - cloudfrontAccessors[id] = [...actions].sort(); - } - } - - const callFor = (operation: string) => { - return { - physicalResourceId: PhysicalResourceId.of("xa-mgmt-lambda-caller"), - service: "Lambda", - action: "InvokeFunction", - parameters: { - FunctionName: this.function.functionName, - InvocationType: "Event", - Payload: JSON.stringify({ - operation, - cloudfrontAccessors, - }), - }, - }; - }; - - new AwsCustomResource(this, "xa-mgmt-lambda-caller", { - onCreate: callFor("create"), - onUpdate: callFor("update"), - onDelete: callFor("delete"), - policy: AwsCustomResourcePolicy.fromSdkCalls({ - resources: [this.function.functionArn], - }), - timeout: Duration.seconds(callerTimeout), + const { xaBucketName, xaAwsId, managerTimeout, callerTimeout } = props; + super(scope, id, { + resourceIdentifier: xaBucketName, + xaAwsId, + managerTimeout, + callerTimeout, }); } @@ -161,18 +68,20 @@ export class CrossAccountS3BucketManager extends Construct { * Optionally specify a list of actions (default: ["s3:GetObject"]) */ public static allowCloudfront( + context: Construct, bucketName: string, cloudfrontId: string, actions?: string[], ) { - if (this.cloudfrontRegistry[bucketName] === false) { - throw new Error( - `Cannot register resources for bucket ${bucketName} manager after creation.`, - ); - } + const stack = Stack.of(context); + const caller = this; actions ??= ["s3:GetObject"]; - this.cloudfrontRegistry[bucketName] ??= new Set(); - this.cloudfrontRegistry[bucketName].add(cloudfrontId); - this.cloudfrontPermissionsRegistry[cloudfrontId] = new Set(actions); + super.registerCloudfrontAccessor( + stack, + caller, + bucketName, + cloudfrontId, + actions, + ); } } From 4a203fd0cd6487b908d42be72a8767c9a5aac6d4 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Wed, 5 Nov 2025 08:46:01 +0900 Subject: [PATCH 11/53] Standardize naming, clean up imports --- lib/s3/index.ts | 4 ++-- lib/s3/{updater.ts => manager.ts} | 25 ++----------------------- 2 files changed, 4 insertions(+), 25 deletions(-) rename lib/s3/{updater.ts => manager.ts} (79%) diff --git a/lib/s3/index.ts b/lib/s3/index.ts index 2d21ea1..e604058 100644 --- a/lib/s3/index.ts +++ b/lib/s3/index.ts @@ -1,5 +1,5 @@ export { CrossAccountS3Bucket } from "./bucket"; export type { CrossAccountS3BucketProps } from "./bucket"; -export { CrossAccountS3BucketManager } from "./updater"; -export type { CrossAccountS3BucketManagerProps } from "./updater"; +export { CrossAccountS3BucketManager } from "./manager"; +export type { CrossAccountS3BucketManagerProps } from "./manager"; diff --git a/lib/s3/updater.ts b/lib/s3/manager.ts similarity index 79% rename from lib/s3/updater.ts rename to lib/s3/manager.ts index 8a8e05e..7fdc97d 100644 --- a/lib/s3/updater.ts +++ b/lib/s3/manager.ts @@ -1,27 +1,6 @@ -import { Duration, Stack } from "aws-cdk-lib"; -import { - Effect, - PolicyDocument, - PolicyStatement, - Role, - ServicePrincipal, -} from "aws-cdk-lib/aws-iam"; -import { Code, Function, Runtime } from "aws-cdk-lib/aws-lambda"; -import { - AwsCustomResource, - AwsCustomResourcePolicy, - PhysicalResourceId, -} from "aws-cdk-lib/custom-resources"; +import { Stack } from "aws-cdk-lib"; import { Construct } from "constructs"; -import path from "path"; -import { - CrossAccountManager, - CrossAccountManagerProps, -} from "../cross-account"; - -interface Registry { - [key: string]: Set | false; -} +import { CrossAccountManager } from "../cross-account"; export interface CrossAccountS3BucketManagerProps { xaBucketName: string; From 6cd6985b160e49426822781c6784906706747484 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Wed, 5 Nov 2025 08:48:42 +0900 Subject: [PATCH 12/53] Refactor KMS manager to use abstract parent, standardize naming, fix some comments --- lib/kms/index.ts | 4 +- lib/kms/manager.ts | 101 ++++++++++++++++++++++++ lib/kms/updater.ts | 187 --------------------------------------------- lib/s3/manager.ts | 4 +- 4 files changed, 105 insertions(+), 191 deletions(-) create mode 100644 lib/kms/manager.ts delete mode 100644 lib/kms/updater.ts diff --git a/lib/kms/index.ts b/lib/kms/index.ts index d5586e3..3223bba 100644 --- a/lib/kms/index.ts +++ b/lib/kms/index.ts @@ -1,5 +1,5 @@ export { CrossAccountKmsKey } from "./key"; export type { CrossAccountKmsKeyProps } from "./key"; -export { CrossAccountKmsKeyManager } from "./updater"; -export type { CrossAccountKmsKeyManagerProps } from "./updater"; +export { CrossAccountKmsKeyManager } from "./manager"; +export type { CrossAccountKmsKeyManagerProps } from "./manager"; diff --git a/lib/kms/manager.ts b/lib/kms/manager.ts new file mode 100644 index 0000000..4f5c3fe --- /dev/null +++ b/lib/kms/manager.ts @@ -0,0 +1,101 @@ +import { Duration, Stack } from "aws-cdk-lib"; +import { + Effect, + PolicyDocument, + PolicyStatement, + Role, + ServicePrincipal, +} from "aws-cdk-lib/aws-iam"; +import { Code, Function, Runtime } from "aws-cdk-lib/aws-lambda"; +import { + AwsCustomResource, + AwsCustomResourcePolicy, + PhysicalResourceId, +} from "aws-cdk-lib/custom-resources"; +import { Construct } from "constructs"; +import path from "path"; +import { CrossAccountManager } from "../cross-account"; + +interface Registry { + [key: string]: Set | false; +} + +export interface CrossAccountKmsKeyManagerProps { + xaKeyId: string; + xaAwsId: string; + managerTimeout?: number; + callerTimeout?: number; +} + +/** + * CrossAccountKmsKeyManager creates a Lambda function and calls it at deployment + * time using an AwsCustomResource (Lambda:InvokeFunction). + * + * @remarks + * - `xaKeyId` should be the ID of the KMS key in the other account whose + * policy we want to manage. + * - `xaAwsId` should be the ID of the AWS account the aforementioned key + * belongs to. + * - `managerTimeout` is optional and specifies the number of seconds for the manager + * Lambda Function's timeout (defaults to 30). + * - `callerTimeout` is optional and specifies the number of seconds for the + * AwsCustomResource's timeout (defaults to 30). + * - To use this construct, first register the resources that need to access a given + * KMS key using one of the following static registry functions: + * - allowCloudfront(keyId, cloudfrontId) + */ +export class CrossAccountKmsKeyManager extends CrossAccountManager { + constructor( + scope: Construct, + id: string, + props: CrossAccountKmsKeyManagerProps, + ) { + const { + xaKeyId, + xaAwsId, + managerTimeout, + callerTimeout, // default timeout of 3 seconds is awful short/fragile + } = props; + super(scope, id, { + resourceIdentifier: xaKeyId, + xaAwsId, + managerTimeout, + callerTimeout, + }); + } + + /** + * Grant access to the given cross-account KMS key to the specified Cloudfront + * Distribution ID + * Optionally specify a list of actions + * (default: [ + * "kms:Decrypt", + * "kms:Encrypt", + * "kms:GenerateDataKey*", + * "kms:DescribeKey" + * ]) + */ + public static allowCloudfront( + context: Construct, + keyId: string, + cloudfrontId: string, + actions?: string[], + ) { + actions ??= [ + "kms:Decrypt", + "kms:Encrypt", + "kms:GenerateDataKey*", + "kms:DescribeKey", + ]; + const stack = Stack.of(context); + const caller = this; + actions ??= ["s3:GetObject"]; + super.registerCloudfrontAccessor( + stack, + caller, + keyId, + cloudfrontId, + actions, + ); + } +} diff --git a/lib/kms/updater.ts b/lib/kms/updater.ts deleted file mode 100644 index 5b1d86a..0000000 --- a/lib/kms/updater.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { Duration, Stack } from "aws-cdk-lib"; -import { - Effect, - PolicyDocument, - PolicyStatement, - Role, - ServicePrincipal, -} from "aws-cdk-lib/aws-iam"; -import { Code, Function, Runtime } from "aws-cdk-lib/aws-lambda"; -import { - AwsCustomResource, - AwsCustomResourcePolicy, - PhysicalResourceId, -} from "aws-cdk-lib/custom-resources"; -import { Construct } from "constructs"; -import path from "path"; - -interface Registry { - [key: string]: Set | false; -} - -export interface CrossAccountKmsKeyManagerProps { - keyId: string; - keyAwsId: string; - managerTimeout?: number; - callerTimeout?: number; -} - -/** - * CrossAccountKmsKeyManager creates a Lambda function and calls it at deployment - * time using an AwsCustomResource (Lambda:InvokeFunction). - * - * @remarks - * - `keyId` should be the ID of the KMS key in the other account whose - * policy we want to manage. - * - `keyAwsId` should be the ID of the AWS account the aforementioned key - * belongs to. - * - `managerTimeout` is optional and specifies the number of seconds for the manager - * Lambda Function's timeout (defaults to 30). - * - `callerTimeout` is optional and specifies the number of seconds for the - * AwsCustomResource's timeout (defaults to 30). - * - To use this construct, first register the resources that need to access a given - * KMS key using one of the following static registry functions: - * - allowCloudfront(keyId, cloudfrontId) - */ -export class CrossAccountKmsKeyManager extends Construct { - /** Static registry mapping keyIds to CloudFront Distribution IDs that need access */ - private static cloudfrontRegistry: Registry = {}; - /** Static registry mapping to CloudFront Distribution IDs to KMS permissions */ - private static cloudfrontPermissionsRegistry: Registry = {}; - - /** - * Returns a sorted list of the set of Cloudfront Distribution IDs in the registry and - * sets the manager to created - */ - private static consumeCloudfrontAccessors(keyId: string): string[] { - const accessors = [ - ...(this.cloudfrontRegistry[keyId] ? this.cloudfrontRegistry[keyId] : []), - ].sort(); - this.cloudfrontRegistry[keyId] = false; - return accessors; - } - - /** - * The Lambda Function that will be called to assume a role cross-account and - * manage the KMS Key - */ - public readonly function: Function; - - constructor( - scope: Construct, - id: string, - props: CrossAccountKmsKeyManagerProps, - ) { - super(scope, id); - - const { - keyId, - keyAwsId, - managerTimeout = 30, // default timeout of 3 seconds is awful short/fragile - callerTimeout = 30, // default timeout of 3 seconds is awful short/fragile - } = props; - - const xaMgmtRoleArn = `arn:aws:iam::${keyAwsId}:role/${keyId}-xa-mgmt`; - const assumeXaMgmtRole = new PolicyDocument({ - statements: [ - new PolicyStatement({ - effect: Effect.ALLOW, - actions: ["sts:AssumeRole"], - resources: [xaMgmtRoleArn], - }), - ], - }); - - const role = new Role(this, "xa-mgmt-lambda-role", { - assumedBy: new ServicePrincipal("lambda.amazonaws.com"), - description: `Execution role for ${keyId} manager Lambda function.`, - inlinePolicies: { - assumeXaMgmtRole, - }, - roleName: `${keyId}-xa-mgmt-ex`, - }); - - this.function = new Function(this, "xa-mgmt-lambda", { - code: Code.fromAsset(path.join(__dirname, "lambda-code")), - handler: "main.handler", - runtime: Runtime.PYTHON_3_13, - timeout: Duration.seconds(managerTimeout), - environment: { - XA_MGMT_ROLE_ARN: xaMgmtRoleArn, - KEY_ID: keyId, - ACCESSOR_ACCOUNT_ID: Stack.of(this).account, - ACCESSOR_STACK_NAME: Stack.of(this).stackName, - }, - role, - }); - - const cloudfrontDistributionIds = - CrossAccountKmsKeyManager.consumeCloudfrontAccessors(keyId); - const cloudfrontAccessors: { [key: string]: string[] } = {}; - for (const id of cloudfrontDistributionIds) { - const actions = - CrossAccountKmsKeyManager.cloudfrontPermissionsRegistry[id]; - if (actions) { - cloudfrontAccessors[id] = [...actions].sort(); - } - } - - const callFor = (operation: string) => { - return { - physicalResourceId: PhysicalResourceId.of("xa-mgmt-lambda-caller"), - service: "Lambda", - action: "InvokeFunction", - parameters: { - FunctionName: this.function.functionName, - InvocationType: "Event", - Payload: JSON.stringify({ - operation, - cloudfrontAccessors, - }), - }, - }; - }; - - new AwsCustomResource(this, "xa-mgmt-lambda-caller", { - onCreate: callFor("create"), - onUpdate: callFor("update"), - onDelete: callFor("delete"), - policy: AwsCustomResourcePolicy.fromSdkCalls({ - resources: [this.function.functionArn], - }), - timeout: Duration.seconds(callerTimeout), - }); - } - - /** - * Grant access to the given cross-account KMS key to the specified Cloudfront - * Distribution ID - * Optionally specify a list of actions - * (default: [ - * "kms:Decrypt", - * "kms:Encrypt", - * "kms:GenerateDataKey*", - * "kms:DescribeKey" - * ]) - */ - public static allowCloudfront( - keyId: string, - cloudfrontId: string, - actions?: string[], - ) { - if (this.cloudfrontRegistry[keyId] === false) { - throw new Error( - `Cannot register resources for key ${keyId} manager after creation.`, - ); - } - actions ??= [ - "kms:Decrypt", - "kms:Encrypt", - "kms:GenerateDataKey*", - "kms:DescribeKey", - ]; - this.cloudfrontRegistry[keyId] ??= new Set(); - this.cloudfrontRegistry[keyId].add(cloudfrontId); - this.cloudfrontPermissionsRegistry[cloudfrontId] = new Set(actions); - } -} diff --git a/lib/s3/manager.ts b/lib/s3/manager.ts index 7fdc97d..8d63748 100644 --- a/lib/s3/manager.ts +++ b/lib/s3/manager.ts @@ -14,9 +14,9 @@ export interface CrossAccountS3BucketManagerProps { * time using an AwsCustomResource (Lambda:InvokeFunction). * * @remarks - * - `bucketName` should be the name of the S3 bucket in the other account whose + * - `xaBucketName` should be the name of the S3 bucket in the other account whose * policy we want to manage. - * - `bucketAwsId` should be the ID of the AWS account the aforementioned bucket + * - `xaAwsId` should be the ID of the AWS account the aforementioned bucket * belongs to. * - `managerTimeout` is optional and specifies the number of seconds for the manager * Lambda Function's timeout (defaults to 30). From 531d5113b579d3c039ef9c19e2f4231394521c89 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Fri, 7 Nov 2025 21:17:37 +0900 Subject: [PATCH 13/53] Bug fixes in consumption check and accessor registration --- lib/cross-account/xa-manager-registry.ts | 10 +- lib/cross-account/xa-manager.ts | 8 +- lib/kms/manager.ts | 20 +--- lib/s3/manager.ts | 6 +- ...st.ts => cross-account-s3-manager.test.ts} | 94 +++++++++++-------- 5 files changed, 72 insertions(+), 66 deletions(-) rename test/{cross-account-s3-updater.test.ts => cross-account-s3-manager.test.ts} (72%) diff --git a/lib/cross-account/xa-manager-registry.ts b/lib/cross-account/xa-manager-registry.ts index 8900a2c..7fd9cb4 100644 --- a/lib/cross-account/xa-manager-registry.ts +++ b/lib/cross-account/xa-manager-registry.ts @@ -57,7 +57,7 @@ export const consumeCloudfrontAccessors = ( consumed: true, }); return ensureRegistry(cloudfrontRegistries, stack, manager).filter( - (r) => r.targetIdentifier, + (r) => r.targetIdentifier == targetIdentifier, ); }; @@ -71,16 +71,18 @@ export const addCloudfrontAccessor = ( ) => { if (isConsumed(stack, manager, targetIdentifier)) throw new Error( - `Cannot register resources for ${targetIdentifier} manager after creation` + + `Cannot register resources for ${targetIdentifier} manager after creation ` + `(registering ${distributionId}).`, ); if ( ensureRegistry(cloudfrontRegistries, stack, manager).find( - (r) => r.accessorIdentifier == distributionId, + (r) => + r.targetIdentifier == targetIdentifier && + r.accessorIdentifier == distributionId, ) ) throw new Error( - `Distribution ${distributionId} has already been registered for` + + `Distribution ${distributionId} has already been registered for ` + `${targetIdentifier} manager.`, ); ensureRegistry(cloudfrontRegistries, stack, manager).push({ diff --git a/lib/cross-account/xa-manager.ts b/lib/cross-account/xa-manager.ts index 4499c25..56bb293 100644 --- a/lib/cross-account/xa-manager.ts +++ b/lib/cross-account/xa-manager.ts @@ -28,6 +28,7 @@ export interface CrossAccountManagerProps { xaAwsId: string; managerTimeout?: number; callerTimeout?: number; + subclassDir: string; } export abstract class CrossAccountManager extends Construct { @@ -69,6 +70,7 @@ export abstract class CrossAccountManager extends Construct { xaAwsId, managerTimeout = 30, // default timeout of 3 seconds is awful short/fragile callerTimeout = 30, // default timeout of 3 seconds is awful short/fragile + subclassDir, } = props; const xaMgmtRoleArn = `arn:aws:iam::${xaAwsId}:role/${resourceIdentifier}-xa-mgmt`; @@ -94,7 +96,7 @@ export abstract class CrossAccountManager extends Construct { // Manager Lambda this.mgrFunction = new LambdaFunction(this, "xa-mgmt-lambda", { - code: Code.fromAsset(path.join(__dirname, "lambda-code")), + code: Code.fromAsset(subclassDir), handler: "main.handler", runtime: Runtime.PYTHON_3_13, timeout: Duration.seconds(managerTimeout), @@ -154,15 +156,15 @@ export abstract class CrossAccountManager extends Construct { protected static registerCloudfrontAccessor( stack: Stack, caller: Function, + distributionId: string, resourceIdentifier: string, - cloudfrontId: string, actions: string[], ) { addCloudfrontAccessor( stack, caller, + distributionId, resourceIdentifier, - cloudfrontId, actions, ); } diff --git a/lib/kms/manager.ts b/lib/kms/manager.ts index 4f5c3fe..e19c317 100644 --- a/lib/kms/manager.ts +++ b/lib/kms/manager.ts @@ -1,25 +1,8 @@ -import { Duration, Stack } from "aws-cdk-lib"; -import { - Effect, - PolicyDocument, - PolicyStatement, - Role, - ServicePrincipal, -} from "aws-cdk-lib/aws-iam"; -import { Code, Function, Runtime } from "aws-cdk-lib/aws-lambda"; -import { - AwsCustomResource, - AwsCustomResourcePolicy, - PhysicalResourceId, -} from "aws-cdk-lib/custom-resources"; +import { Stack } from "aws-cdk-lib"; import { Construct } from "constructs"; import path from "path"; import { CrossAccountManager } from "../cross-account"; -interface Registry { - [key: string]: Set | false; -} - export interface CrossAccountKmsKeyManagerProps { xaKeyId: string; xaAwsId: string; @@ -61,6 +44,7 @@ export class CrossAccountKmsKeyManager extends CrossAccountManager { xaAwsId, managerTimeout, callerTimeout, + subclassDir: path.join(__dirname, "lambda-code"), }); } diff --git a/lib/s3/manager.ts b/lib/s3/manager.ts index 8d63748..042a2b2 100644 --- a/lib/s3/manager.ts +++ b/lib/s3/manager.ts @@ -1,6 +1,7 @@ import { Stack } from "aws-cdk-lib"; import { Construct } from "constructs"; import { CrossAccountManager } from "../cross-account"; +import path from "path"; export interface CrossAccountS3BucketManagerProps { xaBucketName: string; @@ -38,6 +39,7 @@ export class CrossAccountS3BucketManager extends CrossAccountManager { xaAwsId, managerTimeout, callerTimeout, + subclassDir: path.join(__dirname, "lambda-code"), }); } @@ -48,8 +50,8 @@ export class CrossAccountS3BucketManager extends CrossAccountManager { */ public static allowCloudfront( context: Construct, - bucketName: string, cloudfrontId: string, + bucketName: string, actions?: string[], ) { const stack = Stack.of(context); @@ -58,8 +60,8 @@ export class CrossAccountS3BucketManager extends CrossAccountManager { super.registerCloudfrontAccessor( stack, caller, - bucketName, cloudfrontId, + bucketName, actions, ); } diff --git a/test/cross-account-s3-updater.test.ts b/test/cross-account-s3-manager.test.ts similarity index 72% rename from test/cross-account-s3-updater.test.ts rename to test/cross-account-s3-manager.test.ts index 42ac552..9d52a9d 100644 --- a/test/cross-account-s3-updater.test.ts +++ b/test/cross-account-s3-manager.test.ts @@ -35,13 +35,13 @@ test("Single XAS3 Manager", () => { const app = new App(); const stack = new Stack(app, "test-stack"); const managerId = "test-xas3-mgr"; - const bucketName = "test-xas3"; - const bucketAwsId = "098765432109"; + const xaBucketName = "test-xas3"; + const xaAwsId = "098765432109"; // WHEN new CrossAccountS3BucketManager(stack, managerId, { - bucketName, - bucketAwsId, + xaBucketName, + xaAwsId, }); // THEN const template = Template.fromStack(stack); @@ -50,8 +50,8 @@ test("Single XAS3 Manager", () => { Runtime: PYTHON_RUNTIME, }); template.hasResourceProperties("AWS::IAM::Role", { - RoleName: `${bucketName}-xa-mgmt-ex`, - Policies: getPoliciesMatcher(bucketName, bucketAwsId), + RoleName: `${xaBucketName}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(xaBucketName, xaAwsId), }); template.hasResourceProperties("Custom::AWS", {}); }); @@ -60,15 +60,19 @@ test("XAS3 Manager single accessor", () => { const app = new App(); const stack = new Stack(app, "test-stack"); const managerId = "test-xas3-mgr"; - const bucketName = "test-xas3-s"; - const bucketAwsId = "098765432109"; + const xaBucketName = "test-xas3-s"; + const xaAwsId = "098765432109"; const distributionIds = ["SOME_ID"]; // WHEN - CrossAccountS3BucketManager.allowCloudfront(bucketName, distributionIds[0]); + CrossAccountS3BucketManager.allowCloudfront( + stack, + distributionIds[0], + xaBucketName, + ); new CrossAccountS3BucketManager(stack, managerId, { - bucketName, - bucketAwsId, + xaBucketName, + xaAwsId, }); // THEN const template = Template.fromStack(stack); @@ -77,8 +81,8 @@ test("XAS3 Manager single accessor", () => { Runtime: PYTHON_RUNTIME, }); template.hasResourceProperties("AWS::IAM::Role", { - RoleName: `${bucketName}-xa-mgmt-ex`, - Policies: getPoliciesMatcher(bucketName, bucketAwsId), + RoleName: `${xaBucketName}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(xaBucketName, xaAwsId), }); template.hasResourceProperties("Custom::AWS", { Create: getEventMatcher("create", distributionIds), @@ -91,8 +95,8 @@ test("XAS3 Manager multiple accessors", () => { const app = new App(); const stack = new Stack(app, "test-stack"); const managerId = "test-xas3-mgr"; - const bucketName = "test-xas3-m"; - const bucketAwsId = "098765432109"; + const xaBucketName = "test-xas3-m"; + const xaAwsId = "098765432109"; const distributionIds = [ "SOME_ID", "ANOTHER_ID", @@ -102,11 +106,15 @@ test("XAS3 Manager multiple accessors", () => { // WHEN for (const distributionId of distributionIds) { - CrossAccountS3BucketManager.allowCloudfront(bucketName, distributionId); + CrossAccountS3BucketManager.allowCloudfront( + stack, + distributionId, + xaBucketName, + ); } new CrossAccountS3BucketManager(stack, managerId, { - bucketName, - bucketAwsId, + xaBucketName, + xaAwsId, }); // THEN const template = Template.fromStack(stack); @@ -115,8 +123,8 @@ test("XAS3 Manager multiple accessors", () => { Runtime: PYTHON_RUNTIME, }); template.hasResourceProperties("AWS::IAM::Role", { - RoleName: `${bucketName}-xa-mgmt-ex`, - Policies: getPoliciesMatcher(bucketName, bucketAwsId), + RoleName: `${xaBucketName}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(xaBucketName, xaAwsId), }); template.hasResourceProperties("Custom::AWS", { Create: getEventMatcher("create", distributionIds), @@ -129,16 +137,16 @@ test("XAS3 Manager improper usage", () => { const app = new App(); const stack = new Stack(app, "test-stack"); const managerId = "test-xas3-mgr"; - const bucketName = "test-xas3-BAD"; - const bucketAwsId = "098765432109"; + const xaBucketName = "test-xas3-BAD"; + const xaAwsId = "098765432109"; new CrossAccountS3BucketManager(stack, managerId, { - bucketName, - bucketAwsId, + xaBucketName, + xaAwsId, }); expect(() => - CrossAccountS3BucketManager.allowCloudfront(bucketName, "bad :("), + CrossAccountS3BucketManager.allowCloudfront(stack, "bad :(", xaBucketName), ).toThrow(/Cannot register .+ after creation./); }); @@ -146,12 +154,12 @@ test("Multiple XAS3 Managers", () => { const app = new App(); const stack = new Stack(app, "test-stack"); const managerId1 = "test-xas3-mgr1"; - const bucketName1 = "test-xas3-s1"; - const bucketAwsId1 = "123456789012"; + const xaBucketName1 = "test-xas3-s1"; + const xaAwsId1 = "123456789012"; const distributionIds1 = ["SOME_ID"]; const managerId2 = "test-xas3-mgr2"; - const bucketName2 = "test-xas3-m2"; - const bucketAwsId2 = "098765432109"; + const xaBucketName2 = "test-xas3-m2"; + const xaAwsId2 = "098765432109"; const distributionIds2 = [ "SOME_ID", "ANOTHER_ID", @@ -160,17 +168,25 @@ test("Multiple XAS3 Managers", () => { ].sort(); // WHEN - CrossAccountS3BucketManager.allowCloudfront(bucketName1, distributionIds1[0]); + CrossAccountS3BucketManager.allowCloudfront( + stack, + distributionIds1[0], + xaBucketName1, + ); for (const distributionId of distributionIds2) { - CrossAccountS3BucketManager.allowCloudfront(bucketName2, distributionId); + CrossAccountS3BucketManager.allowCloudfront( + stack, + distributionId, + xaBucketName2, + ); } new CrossAccountS3BucketManager(stack, managerId1, { - bucketName: bucketName1, - bucketAwsId: bucketAwsId1, + xaBucketName: xaBucketName1, + xaAwsId: xaAwsId1, }); new CrossAccountS3BucketManager(stack, managerId2, { - bucketName: bucketName2, - bucketAwsId: bucketAwsId2, + xaBucketName: xaBucketName2, + xaAwsId: xaAwsId2, }); // THEN const template = Template.fromStack(stack); @@ -179,8 +195,8 @@ test("Multiple XAS3 Managers", () => { Runtime: PYTHON_RUNTIME, }); template.hasResourceProperties("AWS::IAM::Role", { - RoleName: `${bucketName1}-xa-mgmt-ex`, - Policies: getPoliciesMatcher(bucketName1, bucketAwsId1), + RoleName: `${xaBucketName1}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(xaBucketName1, xaAwsId1), }); template.hasResourceProperties("Custom::AWS", { Create: getEventMatcher("create", distributionIds1), @@ -189,8 +205,8 @@ test("Multiple XAS3 Managers", () => { }); template.hasResourceProperties("AWS::IAM::Role", { - RoleName: `${bucketName2}-xa-mgmt-ex`, - Policies: getPoliciesMatcher(bucketName2, bucketAwsId2), + RoleName: `${xaBucketName2}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(xaBucketName2, xaAwsId2), }); template.hasResourceProperties("Custom::AWS", { Create: getEventMatcher("create", distributionIds2), From 6d79c0557de4b441b8f10d01135af7f70b999368 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 09:34:19 +0900 Subject: [PATCH 14/53] Improve customResource event pattern matching --- test/cross-account-s3-manager.test.ts | 47 ++++++++++++++++++--------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/test/cross-account-s3-manager.test.ts b/test/cross-account-s3-manager.test.ts index 9d52a9d..ef8971d 100644 --- a/test/cross-account-s3-manager.test.ts +++ b/test/cross-account-s3-manager.test.ts @@ -19,17 +19,32 @@ const getPoliciesMatcher = (bucketName: string, bucketAwsId: string) => }), ]); -const getEventMatcher = (operation: string, distributionIds: string[]) => - Match.objectEquals({ +const getEventMatcher = ( + operation: string, + managerId: string, + distributionIds: string[], + actions?: string[], +) => { + actions ??= ["s3:GetObject"]; + const functionPattern = managerId.replaceAll("-", "").replaceAll("_", ""); + return Match.objectEquals({ "Fn::Join": Match.arrayEquals([ "", Match.arrayWith([ + Match.stringLikeRegexp(".+"), + Match.objectEquals({ Ref: Match.stringLikeRegexp(functionPattern) }), Match.stringLikeRegexp( - [operation, "cloudfrontAccessors", ...distributionIds].join("(.+)"), + [ + operation, + "cloudfrontAccessors", + ...distributionIds, + ...actions, + ].join("(.+)"), ), ]), ]), }); +}; test("Single XAS3 Manager", () => { const app = new App(); @@ -85,9 +100,9 @@ test("XAS3 Manager single accessor", () => { Policies: getPoliciesMatcher(xaBucketName, xaAwsId), }); template.hasResourceProperties("Custom::AWS", { - Create: getEventMatcher("create", distributionIds), - Update: getEventMatcher("update", distributionIds), - Delete: getEventMatcher("delete", distributionIds), + Create: getEventMatcher("create", managerId, distributionIds), + Update: getEventMatcher("update", managerId, distributionIds), + Delete: getEventMatcher("delete", managerId, distributionIds), }); }); @@ -127,9 +142,9 @@ test("XAS3 Manager multiple accessors", () => { Policies: getPoliciesMatcher(xaBucketName, xaAwsId), }); template.hasResourceProperties("Custom::AWS", { - Create: getEventMatcher("create", distributionIds), - Update: getEventMatcher("update", distributionIds), - Delete: getEventMatcher("delete", distributionIds), + Create: getEventMatcher("create", managerId, distributionIds), + Update: getEventMatcher("update", managerId, distributionIds), + Delete: getEventMatcher("delete", managerId, distributionIds), }); }); @@ -166,6 +181,7 @@ test("Multiple XAS3 Managers", () => { "YET_ANOTHER_ID", "SO_MANY_IDS", ].sort(); + const actions2 = ["s3:GetObject", "s3:PutObject"]; // WHEN CrossAccountS3BucketManager.allowCloudfront( @@ -178,6 +194,7 @@ test("Multiple XAS3 Managers", () => { stack, distributionId, xaBucketName2, + ["s3:GetObject", "s3:PutObject"], ); } new CrossAccountS3BucketManager(stack, managerId1, { @@ -199,9 +216,9 @@ test("Multiple XAS3 Managers", () => { Policies: getPoliciesMatcher(xaBucketName1, xaAwsId1), }); template.hasResourceProperties("Custom::AWS", { - Create: getEventMatcher("create", distributionIds1), - Update: getEventMatcher("update", distributionIds1), - Delete: getEventMatcher("delete", distributionIds1), + Create: getEventMatcher("create", managerId1, distributionIds1), + Update: getEventMatcher("update", managerId1, distributionIds1), + Delete: getEventMatcher("delete", managerId1, distributionIds1), }); template.hasResourceProperties("AWS::IAM::Role", { @@ -209,8 +226,8 @@ test("Multiple XAS3 Managers", () => { Policies: getPoliciesMatcher(xaBucketName2, xaAwsId2), }); template.hasResourceProperties("Custom::AWS", { - Create: getEventMatcher("create", distributionIds2), - Update: getEventMatcher("update", distributionIds2), - Delete: getEventMatcher("delete", distributionIds2), + Create: getEventMatcher("create", managerId2, distributionIds2, actions2), + Update: getEventMatcher("update", managerId2, distributionIds2, actions2), + Delete: getEventMatcher("delete", managerId2, distributionIds2, actions2), }); }); From 24daa6264cff549090f9ad71a7e89f34a6f86385 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 10:44:57 +0900 Subject: [PATCH 15/53] Add XA KMS Key tests --- test/cross-account-kms-key.test.ts | 87 ++++++++++++++++++++++++++++++ test/matchers.ts | 49 +++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 test/cross-account-kms-key.test.ts create mode 100644 test/matchers.ts diff --git a/test/cross-account-kms-key.test.ts b/test/cross-account-kms-key.test.ts new file mode 100644 index 0000000..cc7f647 --- /dev/null +++ b/test/cross-account-kms-key.test.ts @@ -0,0 +1,87 @@ +import { App, Stack } from "aws-cdk-lib/core"; +import { Template } from "aws-cdk-lib/assertions"; + +import { CrossAccountKmsKey } from "../lib"; +import { getAssumeRolePolicyMatcher, getRoleNameMatcher } from "./matchers"; + +test("Single Accessor XAKMS", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const keyId = "test-xakms-single-accessor"; + const alias = "test-xakms-s"; + const xaAwsIds = ["000000000000"]; + // WHEN + new CrossAccountKmsKey(stack, keyId, { + alias, + xaAwsIds, + }); + // THEN + const template = Template.fromStack(stack); + + template.hasResource("AWS::KMS::Key", {}); + template.hasResourceProperties("AWS::KMS::Alias", { + AliasName: `alias/${alias}`, + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: getRoleNameMatcher(keyId), + AssumeRolePolicyDocument: getAssumeRolePolicyMatcher(keyId, xaAwsIds), + }); +}); + +test("Multi Accessor XAKMS", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const keyId = "test-xakms-multi-accessor"; + const alias = "test-xakms-m"; + const xaAwsIds = ["000000000000", "111111111111", "222222222222"]; + // WHEN + new CrossAccountKmsKey(stack, keyId, { + alias, + xaAwsIds, + }); + // THEN + const template = Template.fromStack(stack); + + template.hasResource("AWS::KMS::Key", {}); + template.hasResourceProperties("AWS::KMS::Alias", { + AliasName: `alias/${alias}`, + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: getRoleNameMatcher(keyId), + AssumeRolePolicyDocument: getAssumeRolePolicyMatcher(keyId, xaAwsIds), + }); +}); + +test("Several XAKMSes", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const keyId1 = "test-xakms-1"; + const alias1 = "test-xakms-alias-1"; + const xaAwsIds1 = ["000000000000"]; + const keyId2 = "test-xakms-2"; + const xaAwsIds2 = ["111111111111"]; + // WHEN + new CrossAccountKmsKey(stack, keyId1, { + alias: alias1, + xaAwsIds: xaAwsIds1, + }); + new CrossAccountKmsKey(stack, keyId2, { + xaAwsIds: xaAwsIds2, + }); + // THEN + const template = Template.fromStack(stack); + + template.hasResource("AWS::KMS::Key", {}); + template.hasResourceProperties("AWS::KMS::Alias", { + AliasName: `alias/${alias1}`, + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: getRoleNameMatcher(keyId1), + AssumeRolePolicyDocument: getAssumeRolePolicyMatcher(keyId1, xaAwsIds1), + }); + + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: getRoleNameMatcher(keyId2), + AssumeRolePolicyDocument: getAssumeRolePolicyMatcher(keyId2, xaAwsIds2), + }); +}); diff --git a/test/matchers.ts b/test/matchers.ts new file mode 100644 index 0000000..84d54d2 --- /dev/null +++ b/test/matchers.ts @@ -0,0 +1,49 @@ +import { Match } from "aws-cdk-lib/assertions"; + +export const getRoleNameMatcher = (resourceId: string) => + Match.objectEquals({ + "Fn::Join": Match.arrayEquals([ + "", + Match.arrayEquals([ + Match.objectEquals({ + Ref: Match.stringLikeRegexp( + `^${resourceId.replaceAll("-", "").replaceAll("_", "")}`, + ), + }), + "-xa-mgmt", + ]), + ]), + }); + +export const getAwsPrincipalMatcher = (resourceName: string, awsId: string) => + Match.objectEquals({ + "Fn::Join": Match.arrayEquals([ + "", + Match.arrayEquals([ + `arn:aws:iam::${awsId}:role/`, + Match.objectEquals({ + Ref: Match.stringLikeRegexp(`^${resourceName.replaceAll("-", "")}`), + }), + "-xa-mgmt-ex", + ]), + ]), + }); + +export const getAssumeRolePolicyMatcher = ( + resourceName: string, + xaAwsIds: string[], +) => + Match.objectLike({ + Statement: Match.arrayWith([ + Match.objectLike({ + Action: "sts:AssumeRole", + Principal: Match.objectLike({ + AWS: + xaAwsIds.length == 1 + ? getAwsPrincipalMatcher(resourceName, xaAwsIds[0]) + : xaAwsIds.map((id) => getAwsPrincipalMatcher(resourceName, id)), + }), + }), + ]), + Version: "2012-10-17", + }); From e55d3f93dffcab4023e70b9c26ff95a683445644 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 10:45:26 +0900 Subject: [PATCH 16/53] Remove silly comments --- test/cross-account-s3-bucket.test.ts | 48 ++-------------------------- 1 file changed, 3 insertions(+), 45 deletions(-) diff --git a/test/cross-account-s3-bucket.test.ts b/test/cross-account-s3-bucket.test.ts index 293050c..f073418 100644 --- a/test/cross-account-s3-bucket.test.ts +++ b/test/cross-account-s3-bucket.test.ts @@ -1,49 +1,8 @@ import { App, Stack } from "aws-cdk-lib/core"; -import { Match, Template } from "aws-cdk-lib/assertions"; -import { CrossAccountS3Bucket } from "../lib"; - -const getRoleNameMatcher = (bucketId: string) => - Match.objectEquals({ - "Fn::Join": Match.arrayEquals([ - "", - Match.arrayEquals([ - Match.objectEquals({ - Ref: Match.stringLikeRegexp(`^${bucketId.replaceAll("-", "")}`), - }), - "-xa-mgmt", - ]), - ]), - }); - -const getAwsPrincipalMatcher = (bucketName: string, id: string) => - Match.objectEquals({ - "Fn::Join": Match.arrayEquals([ - "", - Match.arrayEquals([ - `arn:aws:iam::${id}:role/`, - Match.objectEquals({ - Ref: Match.stringLikeRegexp(`^${bucketName.replaceAll("-", "")}`), - }), - "-xa-mgmt-ex", - ]), - ]), - }); +import { Template } from "aws-cdk-lib/assertions"; -const getAssumeRolePolicyMatcher = (bucketName: string, xaAwsIds: string[]) => - Match.objectLike({ - Statement: Match.arrayWith([ - Match.objectLike({ - Action: "sts:AssumeRole", - Principal: Match.objectLike({ - AWS: - xaAwsIds.length == 1 - ? getAwsPrincipalMatcher(bucketName, xaAwsIds[0]) - : xaAwsIds.map((id) => getAwsPrincipalMatcher(bucketName, id)), - }), - }), - ]), - Version: "2012-10-17", - }); +import { CrossAccountS3Bucket } from "../lib"; +import { getAssumeRolePolicyMatcher, getRoleNameMatcher } from "./matchers"; test("Single Accessor XAS3", () => { const app = new App(); @@ -148,7 +107,6 @@ test("Several XAS3s", () => { // THEN const template = Template.fromStack(stack); - // Do me later template.hasResourceProperties("AWS::S3::Bucket", { BucketName: bucketId1, }); From 2a79d31e8a1e6dc8c3ccc5f4eae4380955454b10 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 11:06:55 +0900 Subject: [PATCH 17/53] Add KMS Key manager tests --- lib/cross-account/xa-manager.ts | 38 ++-- test/const.ts | 9 + test/cross-account-kms-manager.test.ts | 229 +++++++++++++++++++++++++ test/cross-account-s3-manager.test.ts | 112 ++++++------ test/matchers.ts | 41 +++++ 5 files changed, 351 insertions(+), 78 deletions(-) create mode 100644 test/const.ts create mode 100644 test/cross-account-kms-manager.test.ts diff --git a/lib/cross-account/xa-manager.ts b/lib/cross-account/xa-manager.ts index 56bb293..a880124 100644 --- a/lib/cross-account/xa-manager.ts +++ b/lib/cross-account/xa-manager.ts @@ -1,23 +1,13 @@ import { Duration, Stack } from "aws-cdk-lib"; -import { - Effect, - PolicyDocument, - PolicyStatement, - Role, - ServicePrincipal, -} from "aws-cdk-lib/aws-iam"; -import { - Code, - Function as LambdaFunction, - Runtime, -} from "aws-cdk-lib/aws-lambda"; +import { Construct } from "constructs"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as lambda from "aws-cdk-lib/aws-lambda"; import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId, } from "aws-cdk-lib/custom-resources"; -import { Construct } from "constructs"; -import path from "path"; + import { addCloudfrontAccessor, consumeCloudfrontAccessors, @@ -36,8 +26,8 @@ export abstract class CrossAccountManager extends Construct { * The Lambda Function that will be called to assume a role cross-account and * manage the resource */ - private mgrFunction: LambdaFunction; - public get function(): LambdaFunction { + private mgrFunction: lambda.Function; + public get function(): lambda.Function { return this.mgrFunction; } @@ -74,10 +64,10 @@ export abstract class CrossAccountManager extends Construct { } = props; const xaMgmtRoleArn = `arn:aws:iam::${xaAwsId}:role/${resourceIdentifier}-xa-mgmt`; - const assumeXaMgmtRolePolicy = new PolicyDocument({ + const assumeXaMgmtRolePolicy = new iam.PolicyDocument({ statements: [ - new PolicyStatement({ - effect: Effect.ALLOW, + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, actions: ["sts:AssumeRole"], resources: [xaMgmtRoleArn], }), @@ -85,8 +75,8 @@ export abstract class CrossAccountManager extends Construct { }); // Manager Lambda execution role - const role = new Role(this, "xa-mgmt-lambda-role", { - assumedBy: new ServicePrincipal("lambda.amazonaws.com"), + const role = new iam.Role(this, "xa-mgmt-lambda-role", { + assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), description: `Execution role for ${resourceIdentifier} manager Lambda function.`, inlinePolicies: { assumeXaMgmtRolePolicy, @@ -95,10 +85,10 @@ export abstract class CrossAccountManager extends Construct { }); // Manager Lambda - this.mgrFunction = new LambdaFunction(this, "xa-mgmt-lambda", { - code: Code.fromAsset(subclassDir), + this.mgrFunction = new lambda.Function(this, "xa-mgmt-lambda", { + code: lambda.Code.fromAsset(subclassDir), handler: "main.handler", - runtime: Runtime.PYTHON_3_13, + runtime: lambda.Runtime.PYTHON_3_13, timeout: Duration.seconds(managerTimeout), environment: { XA_MGMT_ROLE_ARN: xaMgmtRoleArn, diff --git a/test/const.ts b/test/const.ts new file mode 100644 index 0000000..4c368a1 --- /dev/null +++ b/test/const.ts @@ -0,0 +1,9 @@ +export const PYTHON_RUNTIME = "python3.13"; + +export const S3_DEFAULT_ACTIONS = ["s3:GetObject"]; +export const KMS_DEFAULT_ACTIONS = [ + "kms:Decrypt", + "kms:Encrypt", + "kms:GenerateDataKey*", + "kms:DescribeKey", +]; diff --git a/test/cross-account-kms-manager.test.ts b/test/cross-account-kms-manager.test.ts new file mode 100644 index 0000000..b13ede0 --- /dev/null +++ b/test/cross-account-kms-manager.test.ts @@ -0,0 +1,229 @@ +import { App, Stack } from "aws-cdk-lib/core"; +import { Template } from "aws-cdk-lib/assertions"; +import { CrossAccountKmsKeyManager } from "../lib"; + +import { getPoliciesMatcher, getEventMatcher } from "./matchers"; +import { PYTHON_RUNTIME, KMS_DEFAULT_ACTIONS } from "./const"; + +test("Single XAKMS Manager", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const managerId = "test-xakms-mgr"; + const xaKeyId = "test-xakms"; + const xaAwsId = "098765432109"; + + // WHEN + new CrossAccountKmsKeyManager(stack, managerId, { + xaKeyId, + xaAwsId, + }); + // THEN + const template = Template.fromStack(stack); + + template.hasResourceProperties("AWS::Lambda::Function", { + Runtime: PYTHON_RUNTIME, + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: `${xaKeyId}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(xaKeyId, xaAwsId), + }); + template.hasResourceProperties("Custom::AWS", {}); +}); + +test("XAKMS Manager single accessor", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const managerId = "test-xakms-mgr"; + const xaKeyId = "test-xakms"; + const xaAwsId = "098765432109"; + const distributionIds = ["SOME_ID"]; + + // WHEN + CrossAccountKmsKeyManager.allowCloudfront(stack, distributionIds[0], xaKeyId); + new CrossAccountKmsKeyManager(stack, managerId, { + xaKeyId, + xaAwsId, + }); + // THEN + const template = Template.fromStack(stack); + + template.hasResourceProperties("AWS::Lambda::Function", { + Runtime: PYTHON_RUNTIME, + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: `${xaKeyId}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(xaKeyId, xaAwsId), + }); + template.hasResourceProperties("Custom::AWS", { + Create: getEventMatcher( + "create", + managerId, + distributionIds, + KMS_DEFAULT_ACTIONS, + ), + Update: getEventMatcher( + "update", + managerId, + distributionIds, + KMS_DEFAULT_ACTIONS, + ), + Delete: getEventMatcher( + "delete", + managerId, + distributionIds, + KMS_DEFAULT_ACTIONS, + ), + }); +}); + +test("XAKMS Manager multiple accessors", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const managerId = "test-xakms-mgr"; + const xaKeyId = "test-xakms"; + const xaAwsId = "098765432109"; + const distributionIds = [ + "SOME_ID", + "ANOTHER_ID", + "YET_ANOTHER_ID", + "SO_MANY_IDS", + ].sort(); + + // WHEN + for (const distributionId of distributionIds) { + CrossAccountKmsKeyManager.allowCloudfront(stack, distributionId, xaKeyId); + } + new CrossAccountKmsKeyManager(stack, managerId, { + xaKeyId, + xaAwsId, + }); + // THEN + const template = Template.fromStack(stack); + + template.hasResourceProperties("AWS::Lambda::Function", { + Runtime: PYTHON_RUNTIME, + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: `${xaKeyId}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(xaKeyId, xaAwsId), + }); + template.hasResourceProperties("Custom::AWS", { + Create: getEventMatcher( + "create", + managerId, + distributionIds, + KMS_DEFAULT_ACTIONS, + ), + Update: getEventMatcher( + "update", + managerId, + distributionIds, + KMS_DEFAULT_ACTIONS, + ), + Delete: getEventMatcher( + "delete", + managerId, + distributionIds, + KMS_DEFAULT_ACTIONS, + ), + }); +}); + +test("XAKMS Manager improper usage", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const managerId = "test-xakms-mgr"; + const xaKeyId = "test-xakms-BAD"; + const xaAwsId = "098765432109"; + + new CrossAccountKmsKeyManager(stack, managerId, { + xaKeyId, + xaAwsId, + }); + + expect(() => + CrossAccountKmsKeyManager.allowCloudfront(stack, "bad :(", xaKeyId), + ).toThrow(/Cannot register .+ after creation./); +}); + +test("Multiple XAKMS Managers", () => { + const app = new App(); + const stack = new Stack(app, "test-stack"); + const managerId1 = "test-xakms-mgr1"; + const xaKeyId1 = "test-xakms-s1"; + const xaAwsId1 = "123456789012"; + const distributionIds1 = ["SOME_ID"]; + const managerId2 = "test-xakms-mgr2"; + const xaKeyId2 = "test-xakms-m2"; + const xaAwsId2 = "098765432109"; + const distributionIds2 = [ + "SOME_ID", + "ANOTHER_ID", + "YET_ANOTHER_ID", + "SO_MANY_IDS", + ].sort(); + const actions2 = ["kms:ListKeys", ...KMS_DEFAULT_ACTIONS]; + + // WHEN + CrossAccountKmsKeyManager.allowCloudfront( + stack, + distributionIds1[0], + xaKeyId1, + ); + for (const distributionId of distributionIds2) { + CrossAccountKmsKeyManager.allowCloudfront( + stack, + distributionId, + xaKeyId2, + actions2, + ); + } + new CrossAccountKmsKeyManager(stack, managerId1, { + xaKeyId: xaKeyId1, + xaAwsId: xaAwsId1, + }); + new CrossAccountKmsKeyManager(stack, managerId2, { + xaKeyId: xaKeyId2, + xaAwsId: xaAwsId2, + }); + // THEN + const template = Template.fromStack(stack); + + template.hasResourceProperties("AWS::Lambda::Function", { + Runtime: PYTHON_RUNTIME, + }); + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: `${xaKeyId1}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(xaKeyId1, xaAwsId1), + }); + template.hasResourceProperties("Custom::AWS", { + Create: getEventMatcher( + "create", + managerId1, + distributionIds1, + KMS_DEFAULT_ACTIONS, + ), + Update: getEventMatcher( + "update", + managerId1, + distributionIds1, + KMS_DEFAULT_ACTIONS, + ), + Delete: getEventMatcher( + "delete", + managerId1, + distributionIds1, + KMS_DEFAULT_ACTIONS, + ), + }); + + template.hasResourceProperties("AWS::IAM::Role", { + RoleName: `${xaKeyId2}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(xaKeyId2, xaAwsId2), + }); + template.hasResourceProperties("Custom::AWS", { + Create: getEventMatcher("create", managerId2, distributionIds2, actions2), + Update: getEventMatcher("update", managerId2, distributionIds2, actions2), + Delete: getEventMatcher("delete", managerId2, distributionIds2, actions2), + }); +}); diff --git a/test/cross-account-s3-manager.test.ts b/test/cross-account-s3-manager.test.ts index ef8971d..5bd4663 100644 --- a/test/cross-account-s3-manager.test.ts +++ b/test/cross-account-s3-manager.test.ts @@ -1,50 +1,9 @@ import { App, Stack } from "aws-cdk-lib/core"; -import { Match, Template } from "aws-cdk-lib/assertions"; +import { Template } from "aws-cdk-lib/assertions"; import { CrossAccountS3BucketManager } from "../lib"; -const PYTHON_RUNTIME = "python3.13"; - -const getPoliciesMatcher = (bucketName: string, bucketAwsId: string) => - Match.arrayWith([ - Match.objectLike({ - PolicyDocument: Match.objectLike({ - Statement: Match.arrayWith([ - Match.objectEquals({ - Action: "sts:AssumeRole", - Effect: "Allow", - Resource: `arn:aws:iam::${bucketAwsId}:role/${bucketName}-xa-mgmt`, - }), - ]), - }), - }), - ]); - -const getEventMatcher = ( - operation: string, - managerId: string, - distributionIds: string[], - actions?: string[], -) => { - actions ??= ["s3:GetObject"]; - const functionPattern = managerId.replaceAll("-", "").replaceAll("_", ""); - return Match.objectEquals({ - "Fn::Join": Match.arrayEquals([ - "", - Match.arrayWith([ - Match.stringLikeRegexp(".+"), - Match.objectEquals({ Ref: Match.stringLikeRegexp(functionPattern) }), - Match.stringLikeRegexp( - [ - operation, - "cloudfrontAccessors", - ...distributionIds, - ...actions, - ].join("(.+)"), - ), - ]), - ]), - }); -}; +import { getPoliciesMatcher, getEventMatcher } from "./matchers"; +import { PYTHON_RUNTIME, S3_DEFAULT_ACTIONS } from "./const"; test("Single XAS3 Manager", () => { const app = new App(); @@ -100,9 +59,24 @@ test("XAS3 Manager single accessor", () => { Policies: getPoliciesMatcher(xaBucketName, xaAwsId), }); template.hasResourceProperties("Custom::AWS", { - Create: getEventMatcher("create", managerId, distributionIds), - Update: getEventMatcher("update", managerId, distributionIds), - Delete: getEventMatcher("delete", managerId, distributionIds), + Create: getEventMatcher( + "create", + managerId, + distributionIds, + S3_DEFAULT_ACTIONS, + ), + Update: getEventMatcher( + "update", + managerId, + distributionIds, + S3_DEFAULT_ACTIONS, + ), + Delete: getEventMatcher( + "delete", + managerId, + distributionIds, + S3_DEFAULT_ACTIONS, + ), }); }); @@ -142,9 +116,24 @@ test("XAS3 Manager multiple accessors", () => { Policies: getPoliciesMatcher(xaBucketName, xaAwsId), }); template.hasResourceProperties("Custom::AWS", { - Create: getEventMatcher("create", managerId, distributionIds), - Update: getEventMatcher("update", managerId, distributionIds), - Delete: getEventMatcher("delete", managerId, distributionIds), + Create: getEventMatcher( + "create", + managerId, + distributionIds, + S3_DEFAULT_ACTIONS, + ), + Update: getEventMatcher( + "update", + managerId, + distributionIds, + S3_DEFAULT_ACTIONS, + ), + Delete: getEventMatcher( + "delete", + managerId, + distributionIds, + S3_DEFAULT_ACTIONS, + ), }); }); @@ -194,7 +183,7 @@ test("Multiple XAS3 Managers", () => { stack, distributionId, xaBucketName2, - ["s3:GetObject", "s3:PutObject"], + actions2, ); } new CrossAccountS3BucketManager(stack, managerId1, { @@ -216,9 +205,24 @@ test("Multiple XAS3 Managers", () => { Policies: getPoliciesMatcher(xaBucketName1, xaAwsId1), }); template.hasResourceProperties("Custom::AWS", { - Create: getEventMatcher("create", managerId1, distributionIds1), - Update: getEventMatcher("update", managerId1, distributionIds1), - Delete: getEventMatcher("delete", managerId1, distributionIds1), + Create: getEventMatcher( + "create", + managerId1, + distributionIds1, + S3_DEFAULT_ACTIONS, + ), + Update: getEventMatcher( + "update", + managerId1, + distributionIds1, + S3_DEFAULT_ACTIONS, + ), + Delete: getEventMatcher( + "delete", + managerId1, + distributionIds1, + S3_DEFAULT_ACTIONS, + ), }); template.hasResourceProperties("AWS::IAM::Role", { diff --git a/test/matchers.ts b/test/matchers.ts index 84d54d2..a0700ce 100644 --- a/test/matchers.ts +++ b/test/matchers.ts @@ -47,3 +47,44 @@ export const getAssumeRolePolicyMatcher = ( ]), Version: "2012-10-17", }); + +export const getPoliciesMatcher = (resourceName: string, awsId: string) => + Match.arrayWith([ + Match.objectLike({ + PolicyDocument: Match.objectLike({ + Statement: Match.arrayWith([ + Match.objectEquals({ + Action: "sts:AssumeRole", + Effect: "Allow", + Resource: `arn:aws:iam::${awsId}:role/${resourceName}-xa-mgmt`, + }), + ]), + }), + }), + ]); + +export const getEventMatcher = ( + operation: string, + managerId: string, + distributionIds: string[], + actions: string[], +) => { + const functionPattern = managerId.replaceAll("-", "").replaceAll("_", ""); + return Match.objectEquals({ + "Fn::Join": Match.arrayEquals([ + "", + Match.arrayWith([ + Match.stringLikeRegexp(".+"), + Match.objectEquals({ Ref: Match.stringLikeRegexp(functionPattern) }), + Match.stringLikeRegexp( + [ + operation, + "cloudfrontAccessors", + ...distributionIds, + ...actions, + ].join("(.+)"), + ), + ]), + ]), + }); +}; From 409fc97ec1cda6aaa6706c865bb8c15bf65ea436 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 11:28:26 +0900 Subject: [PATCH 18/53] Define props interfaces (kwargs > positional = fact probably) --- lib/cross-account/xa-manager-registry.ts | 48 ++++++++++++++---------- lib/cross-account/xa-manager.ts | 45 +++++++++------------- lib/kms/manager.ts | 47 ++++++++++++----------- lib/s3/manager.ts | 36 ++++++++++-------- 4 files changed, 94 insertions(+), 82 deletions(-) diff --git a/lib/cross-account/xa-manager-registry.ts b/lib/cross-account/xa-manager-registry.ts index 7fd9cb4..dacde3f 100644 --- a/lib/cross-account/xa-manager-registry.ts +++ b/lib/cross-account/xa-manager-registry.ts @@ -25,30 +25,37 @@ const ensureRegistry = ( return stackRegistry.get(manager)!; }; +interface RegistryManagementBaseProps { + stack: Stack; + manager: Function; + targetIdentifier: string; +} + // A map of Stack instance -> Function (manager Construct constuctor) -> cloudfrontAccessors array const cloudfrontRegistries = new WeakMap>(); // A map of Stack instance -> Function (manager Construct constuctor) -> ConsumptionStatus array const consumptionRegistries = new WeakMap>(); +interface IsConsumedProps extends RegistryManagementBaseProps {} + // Checks if the given resource manager for the given stack has been consumed -const isConsumed = ( - stack: Stack, - manager: Function, - targetIdentifier: string, -) => +const isConsumed = (props: IsConsumedProps) => consumptionRegistries - .get(stack) - ?.get(manager) - ?.find((r) => r.targetIdentifier == targetIdentifier)?.consumed ?? false; + .get(props.stack) + ?.get(props.manager) + ?.find((r) => r.targetIdentifier == props.targetIdentifier)?.consumed ?? + false; + +export interface ConsumeCloudfrontAccessorsProps + extends RegistryManagementBaseProps {} // Consume the cloudfrontAccessors for the provided manager construct of the given stack export const consumeCloudfrontAccessors = ( - stack: Stack, - manager: Function, - targetIdentifier: string, + props: ConsumeCloudfrontAccessorsProps, ) => { - if (isConsumed(stack, manager, targetIdentifier)) + const { stack, manager, targetIdentifier } = props; + if (isConsumed({ stack, manager, targetIdentifier })) throw new Error( `Manager for ${targetIdentifier} has already been consumed.`, ); @@ -61,15 +68,18 @@ export const consumeCloudfrontAccessors = ( ); }; +export interface RegisterCloudfrontAccessorProps + extends RegistryManagementBaseProps { + distributionId: string; + actions: string[]; +} + // Manager functions that call this should specify a default list of actions for ergonomics -export const addCloudfrontAccessor = ( - stack: Stack, - manager: Function, - distributionId: string, - targetIdentifier: string, - actions: string[], +export const registerCloudfrontAccessor = ( + props: RegisterCloudfrontAccessorProps, ) => { - if (isConsumed(stack, manager, targetIdentifier)) + const { stack, manager, targetIdentifier, distributionId, actions } = props; + if (isConsumed({ stack, manager, targetIdentifier })) throw new Error( `Cannot register resources for ${targetIdentifier} manager after creation ` + `(registering ${distributionId}).`, diff --git a/lib/cross-account/xa-manager.ts b/lib/cross-account/xa-manager.ts index a880124..d322e03 100644 --- a/lib/cross-account/xa-manager.ts +++ b/lib/cross-account/xa-manager.ts @@ -9,9 +9,16 @@ import { } from "aws-cdk-lib/custom-resources"; import { - addCloudfrontAccessor, - consumeCloudfrontAccessors, + ConsumeCloudfrontAccessorsProps, + RegisterCloudfrontAccessorProps, } from "./xa-manager-registry"; +import * as registry from "./xa-manager-registry"; + +export interface AllowCloudfrontBaseProps { + scope: Construct; + distributionId: string; + actions?: string[]; +} export interface CrossAccountManagerProps { resourceIdentifier: string; @@ -36,15 +43,9 @@ export abstract class CrossAccountManager extends Construct { * the mapping of accessor resource IDs to the permissions they need. */ private static consumeCloudfrontAccessors( - stack: Stack, - caller: Function, - resourceIdentifier: string, + props: ConsumeCloudfrontAccessorsProps, ) { - const accessInfo = consumeCloudfrontAccessors( - stack, - caller, - resourceIdentifier, - ); + const accessInfo = registry.consumeCloudfrontAccessors(props); const accessors: { [accessor: string]: string[] } = {}; for (const access of accessInfo) { accessors[access.accessorIdentifier] = access.permissions; @@ -102,12 +103,12 @@ export abstract class CrossAccountManager extends Construct { // Get the Cloudfront IDs and their permissions for this subclass from the // Registries const stack = Stack.of(this); - const caller = this.constructor; - const cloudfrontAccessors = CrossAccountManager.consumeCloudfrontAccessors( + const manager = this.constructor; + const cloudfrontAccessors = CrossAccountManager.consumeCloudfrontAccessors({ stack, - caller, - resourceIdentifier, - ); + manager, + targetIdentifier: resourceIdentifier, + }); // Util factory to get an AwsSdkCall for the AwsCustomResource const callFor = (operation: string) => { @@ -144,18 +145,8 @@ export abstract class CrossAccountManager extends Construct { * specifying a default list of actions */ protected static registerCloudfrontAccessor( - stack: Stack, - caller: Function, - distributionId: string, - resourceIdentifier: string, - actions: string[], + props: RegisterCloudfrontAccessorProps, ) { - addCloudfrontAccessor( - stack, - caller, - distributionId, - resourceIdentifier, - actions, - ); + registry.registerCloudfrontAccessor(props); } } diff --git a/lib/kms/manager.ts b/lib/kms/manager.ts index e19c317..48d24f1 100644 --- a/lib/kms/manager.ts +++ b/lib/kms/manager.ts @@ -1,7 +1,13 @@ +import path from "path"; + import { Stack } from "aws-cdk-lib"; import { Construct } from "constructs"; -import path from "path"; import { CrossAccountManager } from "../cross-account"; +import { AllowCloudfrontBaseProps } from "../cross-account/xa-manager"; + +export interface AllowCloudfrontProps extends AllowCloudfrontBaseProps { + keyId: string; +} export interface CrossAccountKmsKeyManagerProps { xaKeyId: string; @@ -59,27 +65,26 @@ export class CrossAccountKmsKeyManager extends CrossAccountManager { * "kms:DescribeKey" * ]) */ - public static allowCloudfront( - context: Construct, - keyId: string, - cloudfrontId: string, - actions?: string[], - ) { - actions ??= [ - "kms:Decrypt", - "kms:Encrypt", - "kms:GenerateDataKey*", - "kms:DescribeKey", - ]; - const stack = Stack.of(context); - const caller = this; - actions ??= ["s3:GetObject"]; - super.registerCloudfrontAccessor( - stack, - caller, + public static allowCloudfront(props: AllowCloudfrontProps) { + const { + scope, + distributionId, keyId, - cloudfrontId, + actions = [ + "kms:Decrypt", + "kms:Encrypt", + "kms:GenerateDataKey*", + "kms:DescribeKey", + ], + } = props; + const stack = Stack.of(scope); + const manager = this; + super.registerCloudfrontAccessor({ + stack, + manager, + targetIdentifier: keyId, + distributionId, actions, - ); + }); } } diff --git a/lib/s3/manager.ts b/lib/s3/manager.ts index 042a2b2..ac7d312 100644 --- a/lib/s3/manager.ts +++ b/lib/s3/manager.ts @@ -1,7 +1,13 @@ +import path from "path"; + import { Stack } from "aws-cdk-lib"; import { Construct } from "constructs"; import { CrossAccountManager } from "../cross-account"; -import path from "path"; +import { AllowCloudfrontBaseProps } from "../cross-account/xa-manager"; + +export interface AllowCloudfrontProps extends AllowCloudfrontBaseProps { + bucketName: string; +} export interface CrossAccountS3BucketManagerProps { xaBucketName: string; @@ -48,21 +54,21 @@ export class CrossAccountS3BucketManager extends CrossAccountManager { * Distribution ID * Optionally specify a list of actions (default: ["s3:GetObject"]) */ - public static allowCloudfront( - context: Construct, - cloudfrontId: string, - bucketName: string, - actions?: string[], - ) { - const stack = Stack.of(context); - const caller = this; - actions ??= ["s3:GetObject"]; - super.registerCloudfrontAccessor( - stack, - caller, - cloudfrontId, + public static allowCloudfront(props: AllowCloudfrontProps) { + const { + scope, + distributionId, bucketName, + actions = ["s3:GetObject"], + } = props; + const stack = Stack.of(scope); + const manager = this; + super.registerCloudfrontAccessor({ + stack, + manager, + targetIdentifier: bucketName, + distributionId, actions, - ); + }); } } From e53574da3a23cb270178ecc588092cac66cb21a1 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 11:49:22 +0900 Subject: [PATCH 19/53] Use prop interfaces in tests --- test/cross-account-kms-manager.test.ts | 38 ++++++++++++++-------- test/cross-account-s3-manager.test.ts | 44 ++++++++++++++------------ 2 files changed, 49 insertions(+), 33 deletions(-) diff --git a/test/cross-account-kms-manager.test.ts b/test/cross-account-kms-manager.test.ts index b13ede0..9d4e0db 100644 --- a/test/cross-account-kms-manager.test.ts +++ b/test/cross-account-kms-manager.test.ts @@ -39,7 +39,11 @@ test("XAKMS Manager single accessor", () => { const distributionIds = ["SOME_ID"]; // WHEN - CrossAccountKmsKeyManager.allowCloudfront(stack, distributionIds[0], xaKeyId); + CrossAccountKmsKeyManager.allowCloudfront({ + scope: stack, + distributionId: distributionIds[0], + keyId: xaKeyId, + }); new CrossAccountKmsKeyManager(stack, managerId, { xaKeyId, xaAwsId, @@ -91,7 +95,11 @@ test("XAKMS Manager multiple accessors", () => { // WHEN for (const distributionId of distributionIds) { - CrossAccountKmsKeyManager.allowCloudfront(stack, distributionId, xaKeyId); + CrossAccountKmsKeyManager.allowCloudfront({ + scope: stack, + distributionId, + keyId: xaKeyId, + }); } new CrossAccountKmsKeyManager(stack, managerId, { xaKeyId, @@ -142,7 +150,11 @@ test("XAKMS Manager improper usage", () => { }); expect(() => - CrossAccountKmsKeyManager.allowCloudfront(stack, "bad :(", xaKeyId), + CrossAccountKmsKeyManager.allowCloudfront({ + scope: stack, + distributionId: "bad :(", + keyId: xaKeyId, + }), ).toThrow(/Cannot register .+ after creation./); }); @@ -165,18 +177,18 @@ test("Multiple XAKMS Managers", () => { const actions2 = ["kms:ListKeys", ...KMS_DEFAULT_ACTIONS]; // WHEN - CrossAccountKmsKeyManager.allowCloudfront( - stack, - distributionIds1[0], - xaKeyId1, - ); + CrossAccountKmsKeyManager.allowCloudfront({ + scope: stack, + distributionId: distributionIds1[0], + keyId: xaKeyId1, + }); for (const distributionId of distributionIds2) { - CrossAccountKmsKeyManager.allowCloudfront( - stack, + CrossAccountKmsKeyManager.allowCloudfront({ + scope: stack, distributionId, - xaKeyId2, - actions2, - ); + keyId: xaKeyId2, + actions: actions2, + }); } new CrossAccountKmsKeyManager(stack, managerId1, { xaKeyId: xaKeyId1, diff --git a/test/cross-account-s3-manager.test.ts b/test/cross-account-s3-manager.test.ts index 5bd4663..18cc9e7 100644 --- a/test/cross-account-s3-manager.test.ts +++ b/test/cross-account-s3-manager.test.ts @@ -39,11 +39,11 @@ test("XAS3 Manager single accessor", () => { const distributionIds = ["SOME_ID"]; // WHEN - CrossAccountS3BucketManager.allowCloudfront( - stack, - distributionIds[0], - xaBucketName, - ); + CrossAccountS3BucketManager.allowCloudfront({ + scope: stack, + distributionId: distributionIds[0], + bucketName: xaBucketName, + }); new CrossAccountS3BucketManager(stack, managerId, { xaBucketName, xaAwsId, @@ -95,11 +95,11 @@ test("XAS3 Manager multiple accessors", () => { // WHEN for (const distributionId of distributionIds) { - CrossAccountS3BucketManager.allowCloudfront( - stack, + CrossAccountS3BucketManager.allowCloudfront({ + scope: stack, distributionId, - xaBucketName, - ); + bucketName: xaBucketName, + }); } new CrossAccountS3BucketManager(stack, managerId, { xaBucketName, @@ -150,7 +150,11 @@ test("XAS3 Manager improper usage", () => { }); expect(() => - CrossAccountS3BucketManager.allowCloudfront(stack, "bad :(", xaBucketName), + CrossAccountS3BucketManager.allowCloudfront({ + scope: stack, + distributionId: "bad :(", + bucketName: xaBucketName, + }), ).toThrow(/Cannot register .+ after creation./); }); @@ -173,18 +177,18 @@ test("Multiple XAS3 Managers", () => { const actions2 = ["s3:GetObject", "s3:PutObject"]; // WHEN - CrossAccountS3BucketManager.allowCloudfront( - stack, - distributionIds1[0], - xaBucketName1, - ); + CrossAccountS3BucketManager.allowCloudfront({ + scope: stack, + distributionId: distributionIds1[0], + bucketName: xaBucketName1, + }); for (const distributionId of distributionIds2) { - CrossAccountS3BucketManager.allowCloudfront( - stack, + CrossAccountS3BucketManager.allowCloudfront({ + scope: stack, distributionId, - xaBucketName2, - actions2, - ); + bucketName: xaBucketName2, + actions: actions2, + }); } new CrossAccountS3BucketManager(stack, managerId1, { xaBucketName: xaBucketName1, From 52d01d51419ed38352231b74f639813532e0f55a Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 12:10:09 +0900 Subject: [PATCH 20/53] Add multi-stack test --- test/cross-account-multi-stack.test.ts | 76 ++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 test/cross-account-multi-stack.test.ts diff --git a/test/cross-account-multi-stack.test.ts b/test/cross-account-multi-stack.test.ts new file mode 100644 index 0000000..39d7b5e --- /dev/null +++ b/test/cross-account-multi-stack.test.ts @@ -0,0 +1,76 @@ +import { App, Stack } from "aws-cdk-lib/core"; +import { Template } from "aws-cdk-lib/assertions"; +import * as cloudfront from "aws-cdk-lib/aws-cloudfront"; +import * as origins from "aws-cdk-lib/aws-cloudfront-origins"; +import { CrossAccountS3Bucket, CrossAccountS3BucketManager } from "../lib"; + +import { + getPoliciesMatcher, + getEventMatcher, + getRoleNameMatcher, + getAssumeRolePolicyMatcher, +} from "./matchers"; +import { + PYTHON_RUNTIME, + S3_DEFAULT_ACTIONS, + KMS_DEFAULT_ACTIONS, +} from "./const"; + +test("Cloudfront to S3 (SSE:S3)", () => { + const app = new App(); + const accessedStack = new Stack(app, "accessed-stack"); + const accessedAwsId = "000000000000"; + const xaBucketId = "test-xas3-id"; + const xaBucketName = "test-xas3"; + const accessorStack = new Stack(app, "accessor-stack"); + const accessorAwsId = "111111111111"; + const distributionId = "test-accessor-distribution"; + const managerId = "test-xas3-mgr"; + + // WHEN + const bucket = new CrossAccountS3Bucket(accessedStack, xaBucketId, { + bucketName: xaBucketName, + xaAwsIds: [accessorAwsId], + }); + const distribution = new cloudfront.Distribution( + accessorStack, + distributionId, + { + defaultBehavior: { + origin: new origins.HttpOrigin("http://asickapplication/"), + }, + }, + ); + CrossAccountS3BucketManager.allowCloudfront({ + scope: distribution, + bucketName: xaBucketName, + distributionId: distribution.distributionId, + }); + new CrossAccountS3BucketManager(accessorStack, managerId, { + xaBucketName, + xaAwsId: accessedAwsId, + }); + + // THEN + const accessedTemplate = Template.fromStack(accessedStack); + const accessorTemplate = Template.fromStack(accessorStack); + + accessedTemplate.hasResourceProperties("AWS::S3::Bucket", { + BucketName: xaBucketName, + }); + accessedTemplate.hasResourceProperties("AWS::IAM::Role", { + RoleName: getRoleNameMatcher(xaBucketId), + AssumeRolePolicyDocument: getAssumeRolePolicyMatcher(xaBucketName, [ + accessorAwsId, + ]), + }); + + accessorTemplate.hasResourceProperties("AWS::Lambda::Function", { + Runtime: PYTHON_RUNTIME, + }); + accessorTemplate.hasResourceProperties("AWS::IAM::Role", { + RoleName: `${xaBucketName}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(xaBucketName, accessedAwsId), + }); + accessorTemplate.hasResourceProperties("Custom::AWS", {}); +}); From fdda69e60516aee15c49c8799570d90ef7d198d9 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 12:21:40 +0900 Subject: [PATCH 21/53] Add multi-stack test (SSE:KMS) --- test/cross-account-multi-stack.test.ts | 107 ++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 3 deletions(-) diff --git a/test/cross-account-multi-stack.test.ts b/test/cross-account-multi-stack.test.ts index 39d7b5e..5897d17 100644 --- a/test/cross-account-multi-stack.test.ts +++ b/test/cross-account-multi-stack.test.ts @@ -2,7 +2,13 @@ import { App, Stack } from "aws-cdk-lib/core"; import { Template } from "aws-cdk-lib/assertions"; import * as cloudfront from "aws-cdk-lib/aws-cloudfront"; import * as origins from "aws-cdk-lib/aws-cloudfront-origins"; -import { CrossAccountS3Bucket, CrossAccountS3BucketManager } from "../lib"; +import * as s3 from "aws-cdk-lib/aws-s3"; +import { + CrossAccountKmsKey, + CrossAccountKmsKeyManager, + CrossAccountS3Bucket, + CrossAccountS3BucketManager, +} from "../lib"; import { getPoliciesMatcher, @@ -28,7 +34,7 @@ test("Cloudfront to S3 (SSE:S3)", () => { const managerId = "test-xas3-mgr"; // WHEN - const bucket = new CrossAccountS3Bucket(accessedStack, xaBucketId, { + new CrossAccountS3Bucket(accessedStack, xaBucketId, { bucketName: xaBucketName, xaAwsIds: [accessorAwsId], }); @@ -37,7 +43,9 @@ test("Cloudfront to S3 (SSE:S3)", () => { distributionId, { defaultBehavior: { - origin: new origins.HttpOrigin("http://asickapplication/"), + origin: origins.S3BucketOrigin.withOriginAccessControl( + s3.Bucket.fromBucketName(accessedStack, "xa-bucket", xaBucketName), + ), }, }, ); @@ -74,3 +82,96 @@ test("Cloudfront to S3 (SSE:S3)", () => { }); accessorTemplate.hasResourceProperties("Custom::AWS", {}); }); + +test("Cloudfront to S3 (SSE:KMS)", () => { + const app = new App(); + const accessedStack = new Stack(app, "accessed-stack"); + const accessedAwsId = "000000000000"; + const xaBucketId = "test-xas3-id"; + const xaBucketName = "test-xas3"; + const xaKeyId = "test-xakms"; + const xaAlias = "test-xakms-alias"; + const accessorStack = new Stack(app, "accessor-stack"); + const accessorAwsId = "111111111111"; + const distributionId = "test-accessor-distribution"; + const s3ManagerId = "test-xas3-mgr"; + const kmsManagerId = "test-xakms-mgr"; + + // WHEN + const key = new CrossAccountKmsKey(accessedStack, xaKeyId, { + alias: xaAlias, + xaAwsIds: [accessorAwsId], + }); + new CrossAccountS3Bucket(accessedStack, xaBucketId, { + bucketName: xaBucketName, + encryptionKey: key.key, + xaAwsIds: [accessorAwsId], + }); + const distribution = new cloudfront.Distribution( + accessorStack, + distributionId, + { + defaultBehavior: { + origin: origins.S3BucketOrigin.withOriginAccessControl( + s3.Bucket.fromBucketName(accessedStack, "xa-bucket", xaBucketName), + ), + }, + }, + ); + CrossAccountKmsKeyManager.allowCloudfront({ + scope: distribution, + keyId: xaKeyId, + distributionId: distribution.distributionId, + }); + CrossAccountS3BucketManager.allowCloudfront({ + scope: distribution, + bucketName: xaBucketName, + distributionId: distribution.distributionId, + }); + new CrossAccountKmsKeyManager(accessorStack, kmsManagerId, { + xaKeyId, + xaAwsId: accessedAwsId, + }); + new CrossAccountS3BucketManager(accessorStack, s3ManagerId, { + xaBucketName, + xaAwsId: accessedAwsId, + }); + + // THEN + const accessedTemplate = Template.fromStack(accessedStack); + const accessorTemplate = Template.fromStack(accessorStack); + + accessedTemplate.hasResource("AWS::KMS::Key", {}); + accessedTemplate.hasResourceProperties("AWS::KMS::Alias", { + AliasName: `alias/${xaAlias}`, + }); + accessedTemplate.hasResourceProperties("AWS::IAM::Role", { + RoleName: getRoleNameMatcher(xaKeyId), + AssumeRolePolicyDocument: getAssumeRolePolicyMatcher(xaKeyId, [ + accessorAwsId, + ]), + }); + + accessedTemplate.hasResourceProperties("AWS::S3::Bucket", { + BucketName: xaBucketName, + }); + accessedTemplate.hasResourceProperties("AWS::IAM::Role", { + RoleName: getRoleNameMatcher(xaBucketId), + AssumeRolePolicyDocument: getAssumeRolePolicyMatcher(xaBucketName, [ + accessorAwsId, + ]), + }); + + accessorTemplate.hasResourceProperties("AWS::Lambda::Function", { + Runtime: PYTHON_RUNTIME, + }); + accessorTemplate.hasResourceProperties("AWS::IAM::Role", { + RoleName: `${xaBucketName}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(xaBucketName, accessedAwsId), + }); + accessorTemplate.hasResourceProperties("AWS::IAM::Role", { + RoleName: `${xaKeyId}-xa-mgmt-ex`, + Policies: getPoliciesMatcher(xaKeyId, accessedAwsId), + }); + accessorTemplate.hasResourceProperties("Custom::AWS", {}); +}); From aa813f20bcd4779345a63ec853c9438bbad4523c Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 12:34:26 +0900 Subject: [PATCH 22/53] Test customResource call events in multi-stack tests --- test/cross-account-multi-stack.test.ts | 75 +++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/test/cross-account-multi-stack.test.ts b/test/cross-account-multi-stack.test.ts index 5897d17..ff1d050 100644 --- a/test/cross-account-multi-stack.test.ts +++ b/test/cross-account-multi-stack.test.ts @@ -17,11 +17,16 @@ import { getAssumeRolePolicyMatcher, } from "./matchers"; import { + KMS_DEFAULT_ACTIONS, PYTHON_RUNTIME, S3_DEFAULT_ACTIONS, - KMS_DEFAULT_ACTIONS, } from "./const"; +// NOTE: We can't dynamically test on a Cloudfront Distribution's distributionId here +// due to how CDK tokenizes values generated at deployment time +// In an actual deployment scenario these should resolve before being passed into our +// manager Lambdas/custom resources + test("Cloudfront to S3 (SSE:S3)", () => { const app = new App(); const accessedStack = new Stack(app, "accessed-stack"); @@ -52,7 +57,7 @@ test("Cloudfront to S3 (SSE:S3)", () => { CrossAccountS3BucketManager.allowCloudfront({ scope: distribution, bucketName: xaBucketName, - distributionId: distribution.distributionId, + distributionId: distributionId, }); new CrossAccountS3BucketManager(accessorStack, managerId, { xaBucketName, @@ -80,7 +85,26 @@ test("Cloudfront to S3 (SSE:S3)", () => { RoleName: `${xaBucketName}-xa-mgmt-ex`, Policies: getPoliciesMatcher(xaBucketName, accessedAwsId), }); - accessorTemplate.hasResourceProperties("Custom::AWS", {}); + accessorTemplate.hasResourceProperties("Custom::AWS", { + Create: getEventMatcher( + "create", + managerId, + [distributionId], + S3_DEFAULT_ACTIONS, + ), + Update: getEventMatcher( + "update", + managerId, + [distributionId], + S3_DEFAULT_ACTIONS, + ), + Delete: getEventMatcher( + "delete", + managerId, + [distributionId], + S3_DEFAULT_ACTIONS, + ), + }); }); test("Cloudfront to S3 (SSE:KMS)", () => { @@ -121,12 +145,12 @@ test("Cloudfront to S3 (SSE:KMS)", () => { CrossAccountKmsKeyManager.allowCloudfront({ scope: distribution, keyId: xaKeyId, - distributionId: distribution.distributionId, + distributionId: distributionId, }); CrossAccountS3BucketManager.allowCloudfront({ scope: distribution, bucketName: xaBucketName, - distributionId: distribution.distributionId, + distributionId: distributionId, }); new CrossAccountKmsKeyManager(accessorStack, kmsManagerId, { xaKeyId, @@ -169,9 +193,48 @@ test("Cloudfront to S3 (SSE:KMS)", () => { RoleName: `${xaBucketName}-xa-mgmt-ex`, Policies: getPoliciesMatcher(xaBucketName, accessedAwsId), }); + accessorTemplate.hasResourceProperties("Custom::AWS", { + Create: getEventMatcher( + "create", + s3ManagerId, + [distributionId], + S3_DEFAULT_ACTIONS, + ), + Update: getEventMatcher( + "update", + s3ManagerId, + [distributionId], + S3_DEFAULT_ACTIONS, + ), + Delete: getEventMatcher( + "delete", + s3ManagerId, + [distributionId], + S3_DEFAULT_ACTIONS, + ), + }); accessorTemplate.hasResourceProperties("AWS::IAM::Role", { RoleName: `${xaKeyId}-xa-mgmt-ex`, Policies: getPoliciesMatcher(xaKeyId, accessedAwsId), }); - accessorTemplate.hasResourceProperties("Custom::AWS", {}); + accessorTemplate.hasResourceProperties("Custom::AWS", { + Create: getEventMatcher( + "create", + kmsManagerId, + [distributionId], + KMS_DEFAULT_ACTIONS, + ), + Update: getEventMatcher( + "update", + kmsManagerId, + [distributionId], + KMS_DEFAULT_ACTIONS, + ), + Delete: getEventMatcher( + "delete", + kmsManagerId, + [distributionId], + KMS_DEFAULT_ACTIONS, + ), + }); }); From ff5df6f7bf6d02d5c8e57b770bc94eeba6553174 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 13:28:19 +0900 Subject: [PATCH 23/53] Prepare to package --- lib/index.ts => index.ts | 9 ++++----- test/cross-account-kms-key.test.ts | 2 +- test/cross-account-kms-manager.test.ts | 2 +- test/cross-account-multi-stack.test.ts | 8 ++------ test/cross-account-s3-bucket.test.ts | 2 +- test/cross-account-s3-manager.test.ts | 2 +- 6 files changed, 10 insertions(+), 15 deletions(-) rename lib/index.ts => index.ts (81%) diff --git a/lib/index.ts b/index.ts similarity index 81% rename from lib/index.ts rename to index.ts index cc68364..f1f3558 100644 --- a/lib/index.ts +++ b/index.ts @@ -1,11 +1,10 @@ -export { CrossAccountS3Bucket, CrossAccountS3BucketManager } from "./s3"; +export { CrossAccountS3Bucket, CrossAccountS3BucketManager } from "./lib/s3"; +export { CrossAccountKmsKey, CrossAccountKmsKeyManager } from "./lib/kms"; export type { CrossAccountS3BucketProps, CrossAccountS3BucketManagerProps, -} from "./s3"; - -export { CrossAccountKmsKey, CrossAccountKmsKeyManager } from "./kms"; +} from "./lib/s3"; export type { CrossAccountKmsKeyProps, CrossAccountKmsKeyManagerProps, -} from "./kms"; +} from "./lib/kms"; diff --git a/test/cross-account-kms-key.test.ts b/test/cross-account-kms-key.test.ts index cc7f647..6eba9c4 100644 --- a/test/cross-account-kms-key.test.ts +++ b/test/cross-account-kms-key.test.ts @@ -1,7 +1,7 @@ import { App, Stack } from "aws-cdk-lib/core"; import { Template } from "aws-cdk-lib/assertions"; -import { CrossAccountKmsKey } from "../lib"; +import { CrossAccountKmsKey } from "../lib/kms"; import { getAssumeRolePolicyMatcher, getRoleNameMatcher } from "./matchers"; test("Single Accessor XAKMS", () => { diff --git a/test/cross-account-kms-manager.test.ts b/test/cross-account-kms-manager.test.ts index 9d4e0db..47f1d3a 100644 --- a/test/cross-account-kms-manager.test.ts +++ b/test/cross-account-kms-manager.test.ts @@ -1,6 +1,6 @@ import { App, Stack } from "aws-cdk-lib/core"; import { Template } from "aws-cdk-lib/assertions"; -import { CrossAccountKmsKeyManager } from "../lib"; +import { CrossAccountKmsKeyManager } from "../lib/kms"; import { getPoliciesMatcher, getEventMatcher } from "./matchers"; import { PYTHON_RUNTIME, KMS_DEFAULT_ACTIONS } from "./const"; diff --git a/test/cross-account-multi-stack.test.ts b/test/cross-account-multi-stack.test.ts index ff1d050..6ad83f4 100644 --- a/test/cross-account-multi-stack.test.ts +++ b/test/cross-account-multi-stack.test.ts @@ -3,12 +3,8 @@ import { Template } from "aws-cdk-lib/assertions"; import * as cloudfront from "aws-cdk-lib/aws-cloudfront"; import * as origins from "aws-cdk-lib/aws-cloudfront-origins"; import * as s3 from "aws-cdk-lib/aws-s3"; -import { - CrossAccountKmsKey, - CrossAccountKmsKeyManager, - CrossAccountS3Bucket, - CrossAccountS3BucketManager, -} from "../lib"; +import { CrossAccountKmsKey, CrossAccountKmsKeyManager } from "../lib/kms"; +import { CrossAccountS3Bucket, CrossAccountS3BucketManager } from "../lib/s3"; import { getPoliciesMatcher, diff --git a/test/cross-account-s3-bucket.test.ts b/test/cross-account-s3-bucket.test.ts index f073418..8a7aace 100644 --- a/test/cross-account-s3-bucket.test.ts +++ b/test/cross-account-s3-bucket.test.ts @@ -1,7 +1,7 @@ import { App, Stack } from "aws-cdk-lib/core"; import { Template } from "aws-cdk-lib/assertions"; -import { CrossAccountS3Bucket } from "../lib"; +import { CrossAccountS3Bucket } from "../lib/s3"; import { getAssumeRolePolicyMatcher, getRoleNameMatcher } from "./matchers"; test("Single Accessor XAS3", () => { diff --git a/test/cross-account-s3-manager.test.ts b/test/cross-account-s3-manager.test.ts index 18cc9e7..e64b21e 100644 --- a/test/cross-account-s3-manager.test.ts +++ b/test/cross-account-s3-manager.test.ts @@ -1,6 +1,6 @@ import { App, Stack } from "aws-cdk-lib/core"; import { Template } from "aws-cdk-lib/assertions"; -import { CrossAccountS3BucketManager } from "../lib"; +import { CrossAccountS3BucketManager } from "../lib/s3"; import { getPoliciesMatcher, getEventMatcher } from "./matchers"; import { PYTHON_RUNTIME, S3_DEFAULT_ACTIONS } from "./const"; From e7369278fccc20aa3c24453e2cc90fe3e5831ba7 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 13:29:34 +0900 Subject: [PATCH 24/53] Prepare to package --- tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 777f492..5fb2f90 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "moduleResolution": "NodeNext", "isolatedModules": true, "lib": ["es2022"], + "outDir": "dist", "declaration": true, "strict": true, "noImplicitAny": true, @@ -22,5 +23,5 @@ "skipLibCheck": true, "typeRoots": ["./node_modules/@types"] }, - "exclude": ["node_modules", "cdk.out"] + "include": ["lib", "index.ts"] } From 9aa35bcd9102e3fbfae6daf54392a4c0bd2ce7c6 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 13:38:19 +0900 Subject: [PATCH 25/53] Include Lambda code in package --- {lib/kms/lambda-code => lambda-code/kms}/main.py | 0 {lib/s3/lambda-code => lambda-code/s3}/main.py | 0 lib/kms/manager.ts | 2 +- lib/s3/manager.ts | 2 +- package.json | 6 +++++- 5 files changed, 7 insertions(+), 3 deletions(-) rename {lib/kms/lambda-code => lambda-code/kms}/main.py (100%) rename {lib/s3/lambda-code => lambda-code/s3}/main.py (100%) diff --git a/lib/kms/lambda-code/main.py b/lambda-code/kms/main.py similarity index 100% rename from lib/kms/lambda-code/main.py rename to lambda-code/kms/main.py diff --git a/lib/s3/lambda-code/main.py b/lambda-code/s3/main.py similarity index 100% rename from lib/s3/lambda-code/main.py rename to lambda-code/s3/main.py diff --git a/lib/kms/manager.ts b/lib/kms/manager.ts index 48d24f1..7c9c218 100644 --- a/lib/kms/manager.ts +++ b/lib/kms/manager.ts @@ -50,7 +50,7 @@ export class CrossAccountKmsKeyManager extends CrossAccountManager { xaAwsId, managerTimeout, callerTimeout, - subclassDir: path.join(__dirname, "lambda-code"), + subclassDir: path.join("lambda-code", "kms"), }); } diff --git a/lib/s3/manager.ts b/lib/s3/manager.ts index ac7d312..f2e94ba 100644 --- a/lib/s3/manager.ts +++ b/lib/s3/manager.ts @@ -45,7 +45,7 @@ export class CrossAccountS3BucketManager extends CrossAccountManager { xaAwsId, managerTimeout, callerTimeout, - subclassDir: path.join(__dirname, "lambda-code"), + subclassDir: path.join("lambda-code", "s3"), }); } diff --git a/package.json b/package.json index 2ce5f5f..44d5861 100644 --- a/package.json +++ b/package.json @@ -20,5 +20,9 @@ "peerDependencies": { "aws-cdk-lib": "2.215.0", "constructs": "^10.0.0" - } + }, + "files": [ + "dist", + "lambda-code" + ] } From 617cae8e0043e2049df15f60a66278faddc8d6bf Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 13:43:32 +0900 Subject: [PATCH 26/53] Adjust main and types in package.json, ignore dist --- .gitignore | 2 ++ package.json | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 4fb79df..049ae2c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ node_modules cdk.out **/.venv + +dist/ diff --git a/package.json b/package.json index 44d5861..14d3869 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "cross-account", "version": "0.1.0", - "main": "lib/index.js", - "types": "lib/index.d.ts", + "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { "build": "tsc", "watch": "tsc -w", From 5512f854b2612e629e83c5bc2066aea787f77cf1 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 13:46:13 +0900 Subject: [PATCH 27/53] Adjust main and types in package.json, ignore dist --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 5fb2f90..943abc5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,5 +23,5 @@ "skipLibCheck": true, "typeRoots": ["./node_modules/@types"] }, - "include": ["lib", "index.ts"] + "include": ["lib", "index.ts", "lambda-code"] } From b0d01f23e0864f8ba9ca9f61c6c3a59be3f5e8cb Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 16:51:39 +0900 Subject: [PATCH 28/53] Cfn IDs should be PascalCase --- lib/cross-account/xa-manager.ts | 8 ++++---- lib/kms/key.ts | 4 ++-- lib/s3/bucket.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/cross-account/xa-manager.ts b/lib/cross-account/xa-manager.ts index d322e03..a267a30 100644 --- a/lib/cross-account/xa-manager.ts +++ b/lib/cross-account/xa-manager.ts @@ -76,7 +76,7 @@ export abstract class CrossAccountManager extends Construct { }); // Manager Lambda execution role - const role = new iam.Role(this, "xa-mgmt-lambda-role", { + const role = new iam.Role(this, "xamgmtLambdaRole", { assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), description: `Execution role for ${resourceIdentifier} manager Lambda function.`, inlinePolicies: { @@ -86,7 +86,7 @@ export abstract class CrossAccountManager extends Construct { }); // Manager Lambda - this.mgrFunction = new lambda.Function(this, "xa-mgmt-lambda", { + this.mgrFunction = new lambda.Function(this, "xamgmtLambda", { code: lambda.Code.fromAsset(subclassDir), handler: "main.handler", runtime: lambda.Runtime.PYTHON_3_13, @@ -113,7 +113,7 @@ export abstract class CrossAccountManager extends Construct { // Util factory to get an AwsSdkCall for the AwsCustomResource const callFor = (operation: string) => { return { - physicalResourceId: PhysicalResourceId.of("xa-mgmt-lambda-caller"), + physicalResourceId: PhysicalResourceId.of("xamgmtLambdaCaller"), service: "Lambda", action: "InvokeFunction", parameters: { @@ -127,7 +127,7 @@ export abstract class CrossAccountManager extends Construct { }; }; - new AwsCustomResource(this, "xa-mgmt-lambda-caller", { + new AwsCustomResource(this, "xamgmtLambdaCaller", { onCreate: callFor("create"), onUpdate: callFor("update"), onDelete: callFor("delete"), diff --git a/lib/kms/key.ts b/lib/kms/key.ts index eae1a8a..76b7f62 100644 --- a/lib/kms/key.ts +++ b/lib/kms/key.ts @@ -28,14 +28,14 @@ export class CrossAccountKmsKey extends CrossAccountConstruct { const { xaAwsIds, ...keyProps } = props; super(scope, id, { xaAwsIds }); - this.key = new Key(this, "xa-key", keyProps); + this.key = new Key(this, "xaKey", keyProps); this.createManagementRole(this.key.keyId, this.key.keyArn, [ "kms:GetKeyPolicy", "kms:PutKeyPolicy", ]); - new CfnOutput(this, "key-id", { + new CfnOutput(this, "xaKeyId", { value: this.key.keyId, description: "ID of the cross-account managed KMS key", }); diff --git a/lib/s3/bucket.ts b/lib/s3/bucket.ts index 46bd670..d91a951 100644 --- a/lib/s3/bucket.ts +++ b/lib/s3/bucket.ts @@ -36,14 +36,14 @@ export class CrossAccountS3Bucket extends CrossAccountConstruct { ); } - this.bucket = new Bucket(this, "xa-bucket", bucketProps); + this.bucket = new Bucket(this, "xaBucket", bucketProps); this.createManagementRole(this.bucket.bucketName, this.bucket.bucketArn, [ "s3:GetBucketPolicy", "s3:PutBucketPolicy", ]); - new CfnOutput(this, "bucket-name", { + new CfnOutput(this, "xaBucketName", { value: this.bucket.bucketName, description: "Name of the cross-account managed S3 bucket", }); From bb1d6ab3722544aa840bbe96b63969e663f390a3 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 17:08:40 +0900 Subject: [PATCH 29/53] Cfn IDs should be PascalCase (reloaded - actually PascalCase this time) --- lib/cross-account/xa-manager.ts | 6 +++--- lib/kms/key.ts | 4 ++-- lib/s3/bucket.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/cross-account/xa-manager.ts b/lib/cross-account/xa-manager.ts index a267a30..a99a835 100644 --- a/lib/cross-account/xa-manager.ts +++ b/lib/cross-account/xa-manager.ts @@ -86,7 +86,7 @@ export abstract class CrossAccountManager extends Construct { }); // Manager Lambda - this.mgrFunction = new lambda.Function(this, "xamgmtLambda", { + this.mgrFunction = new lambda.Function(this, "XaMgmtLambda", { code: lambda.Code.fromAsset(subclassDir), handler: "main.handler", runtime: lambda.Runtime.PYTHON_3_13, @@ -113,7 +113,7 @@ export abstract class CrossAccountManager extends Construct { // Util factory to get an AwsSdkCall for the AwsCustomResource const callFor = (operation: string) => { return { - physicalResourceId: PhysicalResourceId.of("xamgmtLambdaCaller"), + physicalResourceId: PhysicalResourceId.of("XaMgmtLambdaCaller"), service: "Lambda", action: "InvokeFunction", parameters: { @@ -127,7 +127,7 @@ export abstract class CrossAccountManager extends Construct { }; }; - new AwsCustomResource(this, "xamgmtLambdaCaller", { + new AwsCustomResource(this, "XaMgmtLambdaCaller", { onCreate: callFor("create"), onUpdate: callFor("update"), onDelete: callFor("delete"), diff --git a/lib/kms/key.ts b/lib/kms/key.ts index 76b7f62..9e18f38 100644 --- a/lib/kms/key.ts +++ b/lib/kms/key.ts @@ -28,14 +28,14 @@ export class CrossAccountKmsKey extends CrossAccountConstruct { const { xaAwsIds, ...keyProps } = props; super(scope, id, { xaAwsIds }); - this.key = new Key(this, "xaKey", keyProps); + this.key = new Key(this, "XaKey", keyProps); this.createManagementRole(this.key.keyId, this.key.keyArn, [ "kms:GetKeyPolicy", "kms:PutKeyPolicy", ]); - new CfnOutput(this, "xaKeyId", { + new CfnOutput(this, "XaKeyId", { value: this.key.keyId, description: "ID of the cross-account managed KMS key", }); diff --git a/lib/s3/bucket.ts b/lib/s3/bucket.ts index d91a951..66ce9a3 100644 --- a/lib/s3/bucket.ts +++ b/lib/s3/bucket.ts @@ -36,14 +36,14 @@ export class CrossAccountS3Bucket extends CrossAccountConstruct { ); } - this.bucket = new Bucket(this, "xaBucket", bucketProps); + this.bucket = new Bucket(this, "XaBucket", bucketProps); this.createManagementRole(this.bucket.bucketName, this.bucket.bucketArn, [ "s3:GetBucketPolicy", "s3:PutBucketPolicy", ]); - new CfnOutput(this, "xaBucketName", { + new CfnOutput(this, "XaBucketName", { value: this.bucket.bucketName, description: "Name of the cross-account managed S3 bucket", }); From 3864a51294d55f27b9eb50555264c84e22b31a40 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 17:40:31 +0900 Subject: [PATCH 30/53] Cfn IDs should be PascalCase (reloaded again - actually PascalCase this time but for real though) --- lib/cross-account/xa-construct.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cross-account/xa-construct.ts b/lib/cross-account/xa-construct.ts index 7afe083..3a4b955 100644 --- a/lib/cross-account/xa-construct.ts +++ b/lib/cross-account/xa-construct.ts @@ -53,7 +53,7 @@ export abstract class CrossAccountConstruct extends Construct { policyTarget: string, policyActions: string[], ) { - this.mgmtRole = new Role(this, "xa-mgmt-role", { + this.mgmtRole = new Role(this, "XaMgmtRole", { assumedBy: new AccountRootPrincipal(), // placeholder description: `IAM role to enable cross-account management of policy for ${resourceIdentifier}`, roleName: `${resourceIdentifier}-xa-mgmt`, @@ -82,7 +82,7 @@ export abstract class CrossAccountConstruct extends Construct { }), ); - new CfnOutput(this, "xa-mgmt-role-arn", { + new CfnOutput(this, "XaMgmtRoleArn", { value: this.role.roleArn, description: "ARN of the IAM role used for cross-account resource policy management", From d6c57475241c43e286b3502294a5f89aa97f8833 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sat, 8 Nov 2025 18:02:35 +0900 Subject: [PATCH 31/53] Cfn IDs should be PascalCase (as many times as it takes) --- lib/cross-account/xa-construct.ts | 13 +++++++++---- lib/cross-account/xa-manager.ts | 2 +- lib/kms/manager.ts | 6 +++++- lib/s3/manager.ts | 6 +++++- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/cross-account/xa-construct.ts b/lib/cross-account/xa-construct.ts index 3a4b955..c2e1333 100644 --- a/lib/cross-account/xa-construct.ts +++ b/lib/cross-account/xa-construct.ts @@ -1,6 +1,7 @@ import { Construct } from "constructs"; import { CfnOutput } from "aws-cdk-lib"; import { + AccountPrincipal, AccountRootPrincipal, ArnPrincipal, Effect, @@ -74,11 +75,15 @@ export abstract class CrossAccountConstruct extends Construct { this.role.assumeRolePolicy?.addStatements( new PolicyStatement({ effect: Effect.ALLOW, - principals: this.xaAwsIds.map( - (id) => - new ArnPrincipal(`arn:aws:iam::${id}:role/${accessorRoleName}`), - ), + principals: this.xaAwsIds.map((id) => new AccountPrincipal(id)), actions: ["sts:AssumeRole"], + conditions: { + StringLike: { + "aws:PrincipalArn": this.xaAwsIds.map( + (id) => `arn:aws:iam::${id}:role/${accessorRoleName}`, + ), + }, + }, }), ); diff --git a/lib/cross-account/xa-manager.ts b/lib/cross-account/xa-manager.ts index a99a835..061caf2 100644 --- a/lib/cross-account/xa-manager.ts +++ b/lib/cross-account/xa-manager.ts @@ -76,7 +76,7 @@ export abstract class CrossAccountManager extends Construct { }); // Manager Lambda execution role - const role = new iam.Role(this, "xamgmtLambdaRole", { + const role = new iam.Role(this, "XaMgmtLambdaRole", { assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), description: `Execution role for ${resourceIdentifier} manager Lambda function.`, inlinePolicies: { diff --git a/lib/kms/manager.ts b/lib/kms/manager.ts index 7c9c218..600857d 100644 --- a/lib/kms/manager.ts +++ b/lib/kms/manager.ts @@ -50,7 +50,11 @@ export class CrossAccountKmsKeyManager extends CrossAccountManager { xaAwsId, managerTimeout, callerTimeout, - subclassDir: path.join("lambda-code", "kms"), + subclassDir: path.join( + path.dirname(require.resolve("cross-account/package.json")), + "lambda-code", + "kms", + ), }); } diff --git a/lib/s3/manager.ts b/lib/s3/manager.ts index f2e94ba..0fd525f 100644 --- a/lib/s3/manager.ts +++ b/lib/s3/manager.ts @@ -45,7 +45,11 @@ export class CrossAccountS3BucketManager extends CrossAccountManager { xaAwsId, managerTimeout, callerTimeout, - subclassDir: path.join("lambda-code", "s3"), + subclassDir: path.join( + path.dirname(require.resolve("cross-account/package.json")), + "lambda-code", + "s3", + ), }); } From 3bdcdd34c80061798fc9efd8737f58d3d5285ffa Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sun, 9 Nov 2025 08:29:02 +0900 Subject: [PATCH 32/53] Bug fixes with Lambda permissions (and code) after CF->S3(SSE:S3) test --- .gitignore | 2 ++ lambda-code/kms/main.py | 11 ++++++----- lambda-code/s3/main.py | 11 ++++++----- lib/cross-account/xa-manager.ts | 25 ++++++++++++++++++++++--- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 049ae2c..98f47bc 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ cdk.out **/.venv dist/ + +*.tgz diff --git a/lambda-code/kms/main.py b/lambda-code/kms/main.py index 2f9a60a..bd570fe 100644 --- a/lambda-code/kms/main.py +++ b/lambda-code/kms/main.py @@ -1,6 +1,7 @@ import json -import os import logging +import os + import boto3 from botocore.exceptions import ClientError @@ -22,8 +23,8 @@ ) XA_MGMT_ROLE_ARN = os.environ["XA_MGMT_ROLE_ARN"] -KEY_ID = os.environ["KEY_ID"] -ACCESSOR_AWS_ID = os.environ["ACCESSOR_AWS_ID"] +KEY_ID = os.environ["RESOURCE_ID"] +ACCESSOR_ACCOUNT_ID = os.environ["ACCESSOR_ACCOUNT_ID"] ACCESSOR_STACK_NAME = os.environ["ACCESSOR_STACK_NAME"] logger = logging.getLogger() @@ -82,7 +83,7 @@ def handler(event, context): # First, remove the statements set by this stack key_policy = get_key_policy(kms) - sid_prefix = f"{ACCESSOR_AWS_ID} {ACCESSOR_STACK_NAME} " + sid_prefix = f"{ACCESSOR_ACCOUNT_ID} {ACCESSOR_STACK_NAME} " key_policy["Statement"] = [ statement for statement in key_policy["Statement"] @@ -101,7 +102,7 @@ def handler(event, context): "Resource": "*", "Condition": { "StringEquals": { - "AWS:SourceArn": f"arn:aws:cloudfront::{ACCESSOR_AWS_ID}:distribution/{id}" + "AWS:SourceArn": f"arn:aws:cloudfront::{ACCESSOR_ACCOUNT_ID}:distribution/{id}" } }, } diff --git a/lambda-code/s3/main.py b/lambda-code/s3/main.py index 50bbda5..16a8a13 100644 --- a/lambda-code/s3/main.py +++ b/lambda-code/s3/main.py @@ -1,6 +1,7 @@ import json -import os import logging +import os + import boto3 from botocore.exceptions import ClientError @@ -22,8 +23,8 @@ ) XA_MGMT_ROLE_ARN = os.environ["XA_MGMT_ROLE_ARN"] -BUCKET_NAME = os.environ["BUCKET_NAME"] -ACCESSOR_AWS_ID = os.environ["ACCESSOR_AWS_ID"] +BUCKET_NAME = os.environ["RESOURCE_ID"] +ACCESSOR_ACCOUNT_ID = os.environ["ACCESSOR_ACCOUNT_ID"] ACCESSOR_STACK_NAME = os.environ["ACCESSOR_STACK_NAME"] logger = logging.getLogger() @@ -85,7 +86,7 @@ def handler(event, context): # First, remove the statements set by this stack bucket_policy = get_bucket_policy(s3) - sid_prefix = f"{ACCESSOR_AWS_ID} {ACCESSOR_STACK_NAME} " + sid_prefix = f"{ACCESSOR_ACCOUNT_ID} {ACCESSOR_STACK_NAME} " bucket_policy["Statement"] = [ statement for statement in bucket_policy["Statement"] @@ -104,7 +105,7 @@ def handler(event, context): "Resource": f"arn:aws:s3:::{BUCKET_NAME}/*", "Condition": { "StringEquals": { - "AWS:SourceArn": f"arn:aws:cloudfront::{ACCESSOR_AWS_ID}:distribution/{id}" + "AWS:SourceArn": f"arn:aws:cloudfront::{ACCESSOR_ACCOUNT_ID}:distribution/{id}" } }, } diff --git a/lib/cross-account/xa-manager.ts b/lib/cross-account/xa-manager.ts index 061caf2..a77f1d4 100644 --- a/lib/cross-account/xa-manager.ts +++ b/lib/cross-account/xa-manager.ts @@ -1,3 +1,4 @@ +import { createHash } from "crypto"; import { Duration, Stack } from "aws-cdk-lib"; import { Construct } from "constructs"; import * as iam from "aws-cdk-lib/aws-iam"; @@ -28,6 +29,17 @@ export interface CrossAccountManagerProps { subclassDir: string; } +const hashAccessors = (accessors: { [accessor: string]: string[] }) => { + const snapshot = JSON.stringify( + Object.fromEntries( + Object.entries(accessors) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([k, v]) => [k, v.sort()]), + ), + ); + return createHash("sha256").update(snapshot).digest("hex").slice(0, 16); +}; + export abstract class CrossAccountManager extends Construct { /** * The Lambda Function that will be called to assume a role cross-account and @@ -82,6 +94,11 @@ export abstract class CrossAccountManager extends Construct { inlinePolicies: { assumeXaMgmtRolePolicy, }, + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName( + "service-role/AWSLambdaBasicExecutionRole", + ), + ], roleName: `${resourceIdentifier}-xa-mgmt-ex`, }); @@ -109,13 +126,14 @@ export abstract class CrossAccountManager extends Construct { manager, targetIdentifier: resourceIdentifier, }); + const cloudfrontAccessorsHash = hashAccessors(cloudfrontAccessors); // Util factory to get an AwsSdkCall for the AwsCustomResource const callFor = (operation: string) => { return { - physicalResourceId: PhysicalResourceId.of("XaMgmtLambdaCaller"), + physicalResourceId: PhysicalResourceId.of(cloudfrontAccessorsHash), service: "Lambda", - action: "InvokeFunction", + action: "Invoke", parameters: { FunctionName: this.function.functionName, InvocationType: "Event", @@ -127,7 +145,7 @@ export abstract class CrossAccountManager extends Construct { }; }; - new AwsCustomResource(this, "XaMgmtLambdaCaller", { + const caller = new AwsCustomResource(this, "XaMgmtLambdaCaller", { onCreate: callFor("create"), onUpdate: callFor("update"), onDelete: callFor("delete"), @@ -136,6 +154,7 @@ export abstract class CrossAccountManager extends Construct { }), timeout: Duration.seconds(callerTimeout), }); + this.function.grantInvoke(caller.grantPrincipal); } /** From 627a0158aa1e95c76602690ebcaee83927e88157 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sun, 9 Nov 2025 08:39:01 +0900 Subject: [PATCH 33/53] Fix empty bucket policy edge case --- lambda-code/s3/main.py | 16 ++++++++++++++-- lib/s3/bucket.ts | 1 + 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lambda-code/s3/main.py b/lambda-code/s3/main.py index 16a8a13..eb5ccaa 100644 --- a/lambda-code/s3/main.py +++ b/lambda-code/s3/main.py @@ -75,6 +75,15 @@ def put_bucket_policy(s3, policy): raise +def delete_bucket_policy(s3): + try: + s3.delete_bucket_policy(Bucket=BUCKET_NAME) + logger.info("Successfully cleared empty bucket policy") + except ClientError as e: + logger.error(f"Failed to clear bucket policy: {e}") + raise + + def handler(event, context): operation = event["operation"] logger.info(f"operation: {operation}") @@ -111,5 +120,8 @@ def handler(event, context): } bucket_policy["Statement"].append(statement) - # Update the bucket policy - put_bucket_policy(s3, bucket_policy) + # Update the bucket policy (delete if no statements are left) + if len(bucket_policy["Statement"]) > 0: + put_bucket_policy(s3, bucket_policy) + else: + delete_bucket_policy(s3) diff --git a/lib/s3/bucket.ts b/lib/s3/bucket.ts index 66ce9a3..580e4eb 100644 --- a/lib/s3/bucket.ts +++ b/lib/s3/bucket.ts @@ -41,6 +41,7 @@ export class CrossAccountS3Bucket extends CrossAccountConstruct { this.createManagementRole(this.bucket.bucketName, this.bucket.bucketArn, [ "s3:GetBucketPolicy", "s3:PutBucketPolicy", + "s3:DeleteBucketPolicy", ]); new CfnOutput(this, "XaBucketName", { From 3cdb9d97fc949592ab15e0787d938be922f51483 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sun, 9 Nov 2025 10:21:34 +0900 Subject: [PATCH 34/53] Output keyArn to facilitate importing in other stack --- lib/kms/key.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/kms/key.ts b/lib/kms/key.ts index 9e18f38..e6e1f2b 100644 --- a/lib/kms/key.ts +++ b/lib/kms/key.ts @@ -35,9 +35,9 @@ export class CrossAccountKmsKey extends CrossAccountConstruct { "kms:PutKeyPolicy", ]); - new CfnOutput(this, "XaKeyId", { - value: this.key.keyId, - description: "ID of the cross-account managed KMS key", + new CfnOutput(this, "XaKeyArn", { + value: this.key.keyArn, + description: "ARN of the cross-account managed KMS key", }); } } From 1d07f83460f43210bd372fca07e39a211db2b337 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sun, 9 Nov 2025 11:29:20 +0900 Subject: [PATCH 35/53] Fix template tests --- test/matchers.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/matchers.ts b/test/matchers.ts index a0700ce..6fe8abf 100644 --- a/test/matchers.ts +++ b/test/matchers.ts @@ -22,7 +22,9 @@ export const getAwsPrincipalMatcher = (resourceName: string, awsId: string) => Match.arrayEquals([ `arn:aws:iam::${awsId}:role/`, Match.objectEquals({ - Ref: Match.stringLikeRegexp(`^${resourceName.replaceAll("-", "")}`), + Ref: Match.stringLikeRegexp( + `^${resourceName.replaceAll("-", "").replaceAll("_", "")}`, + ), }), "-xa-mgmt-ex", ]), @@ -37,11 +39,12 @@ export const getAssumeRolePolicyMatcher = ( Statement: Match.arrayWith([ Match.objectLike({ Action: "sts:AssumeRole", - Principal: Match.objectLike({ - AWS: - xaAwsIds.length == 1 - ? getAwsPrincipalMatcher(resourceName, xaAwsIds[0]) - : xaAwsIds.map((id) => getAwsPrincipalMatcher(resourceName, id)), + Condition: Match.objectEquals({ + StringLike: Match.objectEquals({ + "aws:PrincipalArn": Match.arrayWith( + xaAwsIds.map((id) => getAwsPrincipalMatcher(resourceName, id)), + ), + }), }), }), ]), @@ -81,7 +84,7 @@ export const getEventMatcher = ( operation, "cloudfrontAccessors", ...distributionIds, - ...actions, + ...actions.sort(), ].join("(.+)"), ), ]), From 1da33b39663b19295dc589256ae59b39ef045a14 Mon Sep 17 00:00:00 2001 From: DocHolmes Date: Sun, 9 Nov 2025 13:04:43 +0900 Subject: [PATCH 36/53] Initial module code v1.0.0 --- package.json | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 14d3869..d0529c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,20 @@ { - "name": "cross-account", - "version": "0.1.0", + "name": "xa-cdk", + "version": "1.0.0", + "description": "CDK constructs for facilitating cross-account resource access", + "repository": { + "type": "git", + "url": "https://github.com/DocEight/xa-cdk.git" + }, + "license": "MIT", + "keywords": [ + "aws", + "cdk", + "s3", + "kms", + "cloudfront", + "cross-account" + ], "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { From b6554097063e20a9f5cb5fcd2fa0e7c83f53676c Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 13:44:36 +0900 Subject: [PATCH 37/53] Add API reference and usage guide to README --- README.md | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f87d33a..d589409 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# cross-account resources +# xa-cdk + +## Description This library contains resources that can be configured to be accessible across AWS accounts. By default, CDK can't update the resource policies of resources in another account, so when @@ -9,7 +11,96 @@ No more shall developers be forced to click through dashboard menus (or type CLI tedious in its own right) to simply access their S3 buckets (et al) from another account! FREEDOM!! -# Diagram +## Installation + +TODO + +## Usage + +Given accessed AWS account with ID: `000000000000` +and accessor AWS account with ID: `111111111111`, +you can automate IAM policy management across AWS accounts/CDK stacks using the following pattern: + +1. In `000000000000`: Deploy the resource(s) to be accessed + +```typescript +const key = new xa.CrossAccountKmsKey(this, "xaKey", { + xaAwsIds: ["111111111111"], // AWS account IDs that need access +}); + +new xa.CrossAccountS3Bucket(this, "XaBucket", { + bucketName: "xa-bucket", + xaAwsIds: ["111111111111"], // AWS account IDs that need access + encryptionKey: key.key, +}); +``` + +2. In `111111111111`: Register the resources that need access + +```typescript +// Import the resources to be accessed +const xaBucket = s3.Bucket.fromBucketName( + this, + "XaBucket", + "xa-bucket", +); +const xaKey = kms.Key.fromKeyArn( + this, + "XaKey", + "arn:aws:kms:ap-northeast-1:000000000000:key/00000000-0000-4000-000-000000000000", +); + +// Make the distribution itself +this.distribution = new cloudfront.Distribution(this, "Distribution", { + defaultBehavior: { + origin: origins.S3BucketOrigin.withOriginAccessControl(xaBucket), + }, +}); + +// Register the Cloudfront distribution with the S3 Bucket and KMS key here +xa.CrossAccountS3BucketManager.allowCloudfront({ + scope: this, + bucketName: xaBucket.bucketName, + distributionId: this.distribution.distributionId, +}); +xa.CrossAccountKmsKeyManager.allowCloudfront({ + scope: this, + keyId: xaKey.keyId, + distributionId: this.distribution.distributionId, +}); +``` + +3. In `111111111111`: After all accessors have been registered, create a cross-account resource manager for each resource in the other account + +```typescript +... + +// Create resource managers to update policies on the resources in the other account +new xa.CrossAccountS3BucketManager(this, "XaBucketMgmt", { + xaBucketName: "xa-bucket", + xaAwsId: "000000000000", // AWS account ID of the S3 Bucket +}); +new xa.CrossAccountKmsKeyManager(this, "XaKeyMgmt", { + xaKeyId: "00000000-0000-4000-000-000000000000", + xaAwsId: "000000000000", // AWS account ID of the KMS Key +}); + +``` + +**Note:** Attempting to register new accessors after this step will result in an error. + +## API Reference + +(v1.0.0) + +- Managed S3 Bucket: `CrossAccountS3Bucket(scope: Construct, id: string, { xaAwsIds: string[], ...BucketProps })` +- Managed KMS key: `CrossAccountKmsKey(scope: Construct, id: string, { xaAwsIds: string[], ...KeyProps })` +- S3 Bucket Manager: `CrossAccountS3BucketManager(scope: Construct, id: string, { xaBucketName: string, xaAwsId: string, managerTimeout?: number = 30, callerTimeout?: number = 30 })` + - Register Cloudfront distribution: `static allowCloudfront({ scope: Construct, distributionId: string, bucketName: string, actions?: string[] = ["s3:GetObject"] })` +- KMS Key Manager: `CrossAccountKmsKeyManager(scope: Construct, id: string, { xaKeyId: string, xaAwsId: string, managerTimeout?: number = 30, callerTimeout?: number = 30 })` + - Register Cloudfront distribution: `static allowCloudfront({ scope: Construct, distributionId: string, keyId: string, actions?: string[] = ["kms:Decrypt", "kms:Encrypt", "kms:GenerateDataKey*", "kms:DescribeKey"] })` + +## Diagram ```mermaid @@ -47,3 +138,5 @@ architecture-beta I'm using them now. If they still aren't rendering properly by the time you read this, here's a link to a prerendered .svg diagram as well.) + +## Contributing / License From 2a263dc983c998e0373e9976a2d136f3e71204e2 Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 13:54:46 +0900 Subject: [PATCH 38/53] Rebase with main and add license section --- README.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d589409..94568a9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # xa-cdk +![npm](https://img.shields.io/npm/v/xa-cdk) +![MIT](https://img.shields.io/badge/license-MIT-green) + ## Description This library contains resources that can be configured to be accessible across AWS accounts. @@ -93,12 +96,61 @@ new xa.CrossAccountKmsKeyManager(this, "XaKeyMgmt", { (v1.0.0) -- Managed S3 Bucket: `CrossAccountS3Bucket(scope: Construct, id: string, { xaAwsIds: string[], ...BucketProps })` -- Managed KMS key: `CrossAccountKmsKey(scope: Construct, id: string, { xaAwsIds: string[], ...KeyProps })` -- S3 Bucket Manager: `CrossAccountS3BucketManager(scope: Construct, id: string, { xaBucketName: string, xaAwsId: string, managerTimeout?: number = 30, callerTimeout?: number = 30 })` - - Register Cloudfront distribution: `static allowCloudfront({ scope: Construct, distributionId: string, bucketName: string, actions?: string[] = ["s3:GetObject"] })` -- KMS Key Manager: `CrossAccountKmsKeyManager(scope: Construct, id: string, { xaKeyId: string, xaAwsId: string, managerTimeout?: number = 30, callerTimeout?: number = 30 })` - - Register Cloudfront distribution: `static allowCloudfront({ scope: Construct, distributionId: string, keyId: string, actions?: string[] = ["kms:Decrypt", "kms:Encrypt", "kms:GenerateDataKey*", "kms:DescribeKey"] })` +- Managed S3 Bucket: +```typescript +CrossAccountS3Bucket(scope: Construct, id: string, { + xaAwsIds: string[], + ...s3.BucketProps +}) +``` +- Managed KMS key: +```typescript +CrossAccountKmsKey(scope: Construct, id: string, { + xaAwsIds: string[], + ...kms.KeyProps +}) +``` +- S3 Bucket Manager: +```typescript +CrossAccountS3BucketManager(scope: Construct, id: string, { + xaBucketName: string, + xaAwsId: string, + managerTimeout?: number = 30, + callerTimeout?: number = 30 +}) +``` + - Register Cloudfront distribution: + ```typescript + static allowCloudfront({ + scope: Construct, + distributionId: string, + bucketName: string, + actions?: string[] = ["s3:GetObject"] + }) + ``` +- KMS Key Manager: +```typescript +CrossAccountKmsKeyManager(scope: Construct, id: string, { + xaKeyId: string, + xaAwsId: string, + managerTimeout?: number = 30, + callerTimeout?: number = 30 +}) +``` + - Register Cloudfront distribution: + ```typescript + static allowCloudfront({ + scope: Construct, + distributionId: string, + keyId: string, + actions?: string[] = [ + "kms:Decrypt", + "kms:Encrypt", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + }) + ``` ## Diagram @@ -139,4 +191,6 @@ I'm using them now. If they still aren't rendering properly by the time you read this, here's a link to a prerendered .svg diagram as well.) -## Contributing / License +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. From 3009a5dfabfa412514c6b9d1c90026453ba02882 Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 13:58:24 +0900 Subject: [PATCH 39/53] Add installation instructions and edit name in package.json --- README.md | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 94568a9..3a2cecc 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ FREEDOM!! ## Installation -TODO +Run: +`npm install @doceight/xa-cdk` ## Usage diff --git a/package.json b/package.json index d0529c2..708e3fe 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "xa-cdk", + "name": "@doceight/xa-cdk", "version": "1.0.0", "description": "CDK constructs for facilitating cross-account resource access", "repository": { From 78397d62ff3ad2cb6e010f0c1a79b2d01306e1e4 Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 14:02:27 +0900 Subject: [PATCH 40/53] Newlines in README --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 3a2cecc..543e403 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,14 @@ ## Description -This library contains resources that can be configured to be accessible across AWS accounts. -By default, CDK can't update the resource policies of resources in another account, so when -setting up cross-account access between resources, some manual configuration is required. +This library contains resources that can be configured to be accessible across AWS accounts. +By default, CDK can't update the resource policies of resources in another account, so when +setting up cross-account access between resources, some manual configuration is required. -BUT NO MORE. -No more shall developers be forced to click through dashboard menus (or type CLI commands, -tedious in its own right) to simply access their S3 buckets (et al) from another account! -FREEDOM!! +BUT NO MORE. +No more shall developers be forced to click through dashboard menus (or type CLI commands, +tedious in its own right) to simply access their S3 buckets (et al) from another account! +FREEDOM!! ## Installation @@ -21,9 +21,9 @@ Run: ## Usage -Given accessed AWS account with ID: `000000000000` -and accessor AWS account with ID: `111111111111`, -you can automate IAM policy management across AWS accounts/CDK stacks using the following pattern: +Given accessed AWS account with ID: `000000000000` +and accessor AWS account with ID: `111111111111`, +you can automate IAM policy management across AWS accounts/CDK stacks using the following pattern: 1. In `000000000000`: Deploy the resource(s) to be accessed @@ -187,10 +187,10 @@ architecture-beta ``` -(One of these days GitHub will support logos:aws-icons in its inline Mermaid diagrams, thus -I'm using them now. -If they still aren't rendering properly by the time you read this, here's a link to a prerendered -.svg diagram as well.) +(One of these days GitHub will support logos:aws-icons in its inline Mermaid diagrams, thus +I'm using them now. +If they still aren't rendering properly by the time you read this, here's a link to a prerendered +.svg diagram as well.) ## License From da5ee9ab78307f2d39ca039e8df205038d3674b7 Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 14:03:15 +0900 Subject: [PATCH 41/53] Newlines in README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 543e403..28da8d2 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ Run: Given accessed AWS account with ID: `000000000000` and accessor AWS account with ID: `111111111111`, -you can automate IAM policy management across AWS accounts/CDK stacks using the following pattern: +you can automate IAM policy management across AWS accounts/CDK stacks using the following +pattern: 1. In `000000000000`: Deploy the resource(s) to be accessed From bfd3749a026e15339b9858ff607cbb62e342bdc4 Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 14:05:12 +0900 Subject: [PATCH 42/53] Newlines in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 28da8d2..1f9e039 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ FREEDOM!! ## Installation -Run: +Run: `npm install @doceight/xa-cdk` ## Usage From 5927b8d457d2b3b22f3c1b91d6f9bc76896f7c05 Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 14:38:05 +0900 Subject: [PATCH 43/53] Add CI and husky git hooks --- .github/workflows/test.yml | 25 +++++++++++++++ .github/workflows/version-check.yml | 47 +++++++++++++++++++++++++++++ .husky/pre-commit | 4 +++ .husky/pre-push | 8 +++++ package-lock.json | 26 +++++++++++++--- package.json | 4 ++- 6 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 .github/workflows/version-check.yml create mode 100755 .husky/pre-commit create mode 100644 .husky/pre-push diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f91140a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,25 @@ +name: Test + +on: + pull_request: + branches: + - main + +jobs: + template-test-and-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Install dependencies + run: npm ci + + - name: Run template tests + run: npm run test + + - name: Check that code builds + run: npm run build diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml new file mode 100644 index 0000000..8cca68f --- /dev/null +++ b/.github/workflows/version-check.yml @@ -0,0 +1,47 @@ +name: Version Check + +on: + pull_request: + branches: + - main + +jobs: + version-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Check version bump + id: check-version + run: | + git fetch origin main + git fetch --tags origin main + if ! git diff origin/main package.json | grep '"version":'; then + echo "ERROR: package.json version must be bumped" + exit 1 + fi + VERSION=$(jq -r '.version' package.json) + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Check for git tag on commit + id: check-tag + run: | + # Get tags pointing at HEAD + TAG=$(git tag --points-at HEAD | head -n1) + if [ -z "$TAG" ]; then + echo "ERROR: Commit must be tagged with a version before merging to main" + exit 1 + fi + echo "Found tag(s): $TAG" + echo "tag=$TAG" >> $GITHUB_OUTPUT + + - name: Check that tag matches version + run: | + if [ "$TAG" != "v$VERSION" ]; then + echo "ERROR: Git tag ($TAG) does not match package.json version (v$VERSION)" + exit 1 + fi + echo "Git tag matches package.json version ✅" + env: + VERSION: ${{ steps.check-version.outputs.version }} + TAG: ${{ steps.check-tag.outputs.tag }} diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..879e935 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npm run test diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..7576e28 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,8 @@ +#!/bin/sh +. "$(dirname -- "$0")/_/husky.sh" + +CHANGED=$(git diff origin/main package.json | grep '"version":') +if [ -z "$CHANGED" ]; then + echo "ERROR: You must update package.json version before pushing to main!" + exit 1 +fi diff --git a/package-lock.json b/package-lock.json index 55fffbf..3f9186a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,19 @@ { - "name": "cross-account", - "version": "0.1.0", + "name": "@doceight/xa-cdk", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "cross-account", - "version": "0.1.0", + "name": "@doceight/xa-cdk", + "version": "1.0.0", + "license": "MIT", "devDependencies": { "@types/jest": "^29.5.14", "@types/node": "22.7.9", "aws-cdk-lib": "2.215.0", "constructs": "^10.0.0", + "husky": "^9.1.7", "jest": "^29.7.0", "ts-jest": "^29.2.5", "typescript": "~5.6.3" @@ -2364,6 +2366,22 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", diff --git a/package.json b/package.json index 708e3fe..40f26a2 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,15 @@ "scripts": { "build": "tsc", "watch": "tsc -w", - "test": "jest" + "test": "jest", + "prepare": "husky install" }, "devDependencies": { "@types/jest": "^29.5.14", "@types/node": "22.7.9", "aws-cdk-lib": "2.215.0", "constructs": "^10.0.0", + "husky": "^8.0.0", "jest": "^29.7.0", "ts-jest": "^29.2.5", "typescript": "~5.6.3" From 6ac9fb6a485efbaf01e1eb25bf5f0de483f54d2a Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 14:46:01 +0900 Subject: [PATCH 44/53] Update husky version in package-lock.json --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3f9186a..655b231 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@types/node": "22.7.9", "aws-cdk-lib": "2.215.0", "constructs": "^10.0.0", - "husky": "^9.1.7", + "husky": "^8.0.0", "jest": "^29.7.0", "ts-jest": "^29.2.5", "typescript": "~5.6.3" @@ -2367,16 +2367,16 @@ } }, "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", "dev": true, "license": "MIT", "bin": { - "husky": "bin.js" + "husky": "lib/bin.js" }, "engines": { - "node": ">=18" + "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/typicode" From e9f9a7defe15e9c0fb8090c73386d59a4f915fe4 Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 15:29:57 +0900 Subject: [PATCH 45/53] Remove promise to render svg for now --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 1f9e039..ecc5196 100644 --- a/README.md +++ b/README.md @@ -189,9 +189,7 @@ architecture-beta ``` (One of these days GitHub will support logos:aws-icons in its inline Mermaid diagrams, thus -I'm using them now. -If they still aren't rendering properly by the time you read this, here's a link to a prerendered -.svg diagram as well.) +I'm using them now.) ## License From 782707c7ee8b5c770e593f019676d37f63f60415 Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 15:57:07 +0900 Subject: [PATCH 46/53] Fix package.json --- package.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 40f26a2..78f448b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "CDK constructs for facilitating cross-account resource access", "repository": { "type": "git", - "url": "https://github.com/DocEight/xa-cdk.git" + "url": "git+https://github.com/DocEight/xa-cdk.git" }, "license": "MIT", "keywords": [ @@ -40,5 +40,10 @@ "files": [ "dist", "lambda-code" - ] + ], + "homepage": "https://github.com/DocEight/xa-cdk#readme", + "bugs": { + "url": "https://github.com/DocEight/xa-cdk/issues" + }, + "author": "doceight" } From bc9a7a0e1796c495920ac83bf8b6ec7471dce99f Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 16:05:45 +0900 Subject: [PATCH 47/53] v1.0.0 --- .github/workflows/publish.yml | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..05e79ae --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,37 @@ +name: Publish + +on: + pull_request: + branches: + - main + +permissions: + id-token: write # Required for OIDC + contents: read + +jobs: + publish: + environment: prd + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v6 + with: + node-version: 22 + registry-url: "https://registry.npmjs.org" + + - name: Update npm + run: npm install -g npm@latest + + - name: Install dependencies + run: npm ci + + - name: Run template tests + run: npm run test + + - name: Build module + run: npm run build + + - name: Build module + run: npm publish --access public From 79af98fa7fa3376759e586f317ee7a721175333b Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 16:10:02 +0900 Subject: [PATCH 48/53] Let's not publish on PRs (need a break, that's a bad oversight) --- .github/workflows/publish.yml | 2 +- .husky/pre-push | 0 2 files changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 .husky/pre-push diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 05e79ae..53fb50c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,7 +1,7 @@ name: Publish on: - pull_request: + push: branches: - main diff --git a/.husky/pre-push b/.husky/pre-push old mode 100644 new mode 100755 From d5bd644ddd93a4903efa2a3d67200a8a943bdb09 Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 17:00:30 +0900 Subject: [PATCH 49/53] Fix lambda-code bundling and paths --- lib/kms/manager.ts | 6 +----- lib/s3/manager.ts | 6 +----- package.json | 5 ++--- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/lib/kms/manager.ts b/lib/kms/manager.ts index 600857d..7496d16 100644 --- a/lib/kms/manager.ts +++ b/lib/kms/manager.ts @@ -50,11 +50,7 @@ export class CrossAccountKmsKeyManager extends CrossAccountManager { xaAwsId, managerTimeout, callerTimeout, - subclassDir: path.join( - path.dirname(require.resolve("cross-account/package.json")), - "lambda-code", - "kms", - ), + subclassDir: path.join(__dirname, "../../lambda-code/kms"), }); } diff --git a/lib/s3/manager.ts b/lib/s3/manager.ts index 0fd525f..267153f 100644 --- a/lib/s3/manager.ts +++ b/lib/s3/manager.ts @@ -45,11 +45,7 @@ export class CrossAccountS3BucketManager extends CrossAccountManager { xaAwsId, managerTimeout, callerTimeout, - subclassDir: path.join( - path.dirname(require.resolve("cross-account/package.json")), - "lambda-code", - "s3", - ), + subclassDir: path.join(__dirname, "../../lambda-code/s3"), }); } diff --git a/package.json b/package.json index 78f448b..24fe4ec 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "build": "tsc", + "build": "tsc && cp -r lambda-code dist/", "watch": "tsc -w", "test": "jest", "prepare": "husky install" @@ -38,8 +38,7 @@ "constructs": "^10.0.0" }, "files": [ - "dist", - "lambda-code" + "dist" ], "homepage": "https://github.com/DocEight/xa-cdk#readme", "bugs": { From 1906c1192e15e7b8e099463fad5c2cb27e16ae41 Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 17:12:09 +0900 Subject: [PATCH 50/53] Set package version to 1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 65dc35e..24fe4ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@doceight/xa-cdk", - "version": "0.0.1", + "version": "1.0.0", "description": "CDK constructs for facilitating cross-account resource access", "repository": { "type": "git", From c88db0268636fec977966de5d01526fb41e5e5e6 Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 17:18:02 +0900 Subject: [PATCH 51/53] Fetch tags in CI --- .github/workflows/version-check.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml index 8cca68f..2b2a084 100644 --- a/.github/workflows/version-check.yml +++ b/.github/workflows/version-check.yml @@ -10,6 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 + with: + fetch-depth: 0 + fetch-tags: true - name: Check version bump id: check-version From 13caec46c23ef4951a7e9fac1945c65b88add173 Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 17:24:32 +0900 Subject: [PATCH 52/53] Add comment --- .github/workflows/version-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml index 2b2a084..181ca15 100644 --- a/.github/workflows/version-check.yml +++ b/.github/workflows/version-check.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v5 with: fetch-depth: 0 - fetch-tags: true + fetch-tags: true # required to check tags - name: Check version bump id: check-version From c92d6ef8d031f432fb517adaece57ad520ba9603 Mon Sep 17 00:00:00 2001 From: DocEight Date: Sun, 9 Nov 2025 17:25:56 +0900 Subject: [PATCH 53/53] Not enforcing tags for now --- .github/workflows/version-check.yml | 50 ----------------------------- 1 file changed, 50 deletions(-) delete mode 100644 .github/workflows/version-check.yml diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml deleted file mode 100644 index 181ca15..0000000 --- a/.github/workflows/version-check.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Version Check - -on: - pull_request: - branches: - - main - -jobs: - version-check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - fetch-tags: true # required to check tags - - - name: Check version bump - id: check-version - run: | - git fetch origin main - git fetch --tags origin main - if ! git diff origin/main package.json | grep '"version":'; then - echo "ERROR: package.json version must be bumped" - exit 1 - fi - VERSION=$(jq -r '.version' package.json) - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Check for git tag on commit - id: check-tag - run: | - # Get tags pointing at HEAD - TAG=$(git tag --points-at HEAD | head -n1) - if [ -z "$TAG" ]; then - echo "ERROR: Commit must be tagged with a version before merging to main" - exit 1 - fi - echo "Found tag(s): $TAG" - echo "tag=$TAG" >> $GITHUB_OUTPUT - - - name: Check that tag matches version - run: | - if [ "$TAG" != "v$VERSION" ]; then - echo "ERROR: Git tag ($TAG) does not match package.json version (v$VERSION)" - exit 1 - fi - echo "Git tag matches package.json version ✅" - env: - VERSION: ${{ steps.check-version.outputs.version }} - TAG: ${{ steps.check-tag.outputs.tag }}