@bifravst/http-api-mock 3.0.2 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/npm/cdk/App.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { PackedLambda } from '@bifravst/aws-cdk-lambda-helpers'
2
+ import type { PackedLayer } from '@bifravst/aws-cdk-lambda-helpers/layer'
3
+ import { App } from 'aws-cdk-lib'
4
+ import { HTTPAPIMockStack } from './Stack.ts'
5
+
6
+ export class HTTPAPIMockApp extends App {
7
+ public constructor(
8
+ stackName: string,
9
+ {
10
+ lambdaSources,
11
+ layer,
12
+ }: {
13
+ lambdaSources: {
14
+ httpApiMock: PackedLambda
15
+ }
16
+ layer: PackedLayer
17
+ },
18
+ ) {
19
+ super({
20
+ context: {
21
+ isTest: true,
22
+ },
23
+ })
24
+ new HTTPAPIMockStack(this, stackName, { lambdaSources, layer })
25
+ }
26
+ }
@@ -0,0 +1,66 @@
1
+ import type { PackedLambda } from '@bifravst/aws-cdk-lambda-helpers'
2
+ import { LambdaSource } from '@bifravst/aws-cdk-lambda-helpers/cdk'
3
+ import type { PackedLayer } from '@bifravst/aws-cdk-lambda-helpers/layer'
4
+ import type { App } from 'aws-cdk-lib'
5
+ import { CfnOutput, aws_lambda as Lambda, Stack } from 'aws-cdk-lib'
6
+ import { HttpApiMock } from './resources/HttpApiMock.ts'
7
+
8
+ /**
9
+ * This is CloudFormation stack sets up a dummy HTTP API which stores all requests in SQS for inspection
10
+ */
11
+ export class HTTPAPIMockStack extends Stack {
12
+ public constructor(
13
+ parent: App,
14
+ stackName: string,
15
+ {
16
+ lambdaSources,
17
+ layer,
18
+ }: {
19
+ lambdaSources: {
20
+ httpApiMock: PackedLambda
21
+ }
22
+ layer: PackedLayer
23
+ },
24
+ ) {
25
+ super(parent, stackName, {
26
+ description:
27
+ 'Provides a mock HTTP API for testing third-party API integrations.',
28
+ })
29
+
30
+ const baseLayer = new Lambda.LayerVersion(this, 'baseLayer', {
31
+ layerVersionName: `${Stack.of(this).stackName}-baseLayer`,
32
+ code: new LambdaSource(this, {
33
+ id: 'baseLayer',
34
+ zipFilePath: layer.layerZipFilePath,
35
+ hash: layer.hash,
36
+ }).code,
37
+ compatibleArchitectures: [Lambda.Architecture.ARM_64],
38
+ compatibleRuntimes: [Lambda.Runtime.NODEJS_24_X],
39
+ })
40
+
41
+ const httpMockApi = new HttpApiMock(this, {
42
+ lambdaSources,
43
+ layers: [baseLayer],
44
+ })
45
+
46
+ // Export these so the test runner can use them
47
+ new CfnOutput(this, 'apiURL', {
48
+ value: httpMockApi.api.url,
49
+ exportName: `${this.stackName}:apiURL`,
50
+ })
51
+ new CfnOutput(this, 'responsesTableName', {
52
+ value: httpMockApi.responsesTable.tableName,
53
+ exportName: `${this.stackName}:responsesTableName`,
54
+ })
55
+ new CfnOutput(this, 'requestsTableName', {
56
+ value: httpMockApi.requestsTable.tableName,
57
+ exportName: `${this.stackName}:requestsTableName`,
58
+ })
59
+ }
60
+ }
61
+
62
+ export type StackOutputs = {
63
+ apiURL: string
64
+ requestsTableName: string
65
+ responsesTableName: string
66
+ }
@@ -0,0 +1,109 @@
1
+ import { Toolkit } from '@aws-cdk/toolkit-lib'
2
+ import { CloudFormationClient } from '@aws-sdk/client-cloudformation'
3
+ import { packLambdaFromPath } from '@bifravst/aws-cdk-lambda-helpers'
4
+ import { packLayer } from '@bifravst/aws-cdk-lambda-helpers/layer'
5
+ import { stackOutput } from '@bifravst/cloudformation-helpers'
6
+ import commandLineArgs from 'command-line-args'
7
+ import { writeFileSync } from 'node:fs'
8
+ import fs from 'node:fs/promises'
9
+ import os from 'node:os'
10
+ import path from 'node:path'
11
+ import { fileURLToPath } from 'node:url'
12
+ import pJSON from '../package.json' with { type: 'json' }
13
+ import { randomString } from '../src/randomString.ts'
14
+ import { HTTPAPIMockApp } from './App.ts'
15
+ import type { StackOutputs } from './Stack.ts'
16
+
17
+ const options = commandLineArgs([
18
+ {
19
+ name: 'config',
20
+ type: Boolean,
21
+ defaultValue: false,
22
+ },
23
+ {
24
+ name: 'destroy',
25
+ type: Boolean,
26
+ defaultValue: false,
27
+ },
28
+ ])
29
+
30
+ const loadConfig = async (): Promise<
31
+ Partial<StackOutputs & { stackName: string }>
32
+ > => {
33
+ try {
34
+ const config = JSON.parse(
35
+ await fs.readFile(
36
+ path.join(process.cwd(), 'http-api-mock.json'),
37
+ 'utf-8',
38
+ ),
39
+ )
40
+ return config
41
+ } catch {
42
+ return {}
43
+ }
44
+ }
45
+
46
+ const stackName =
47
+ process.env.HTTP_API_MOCK_STACK_NAME ??
48
+ (await loadConfig())?.stackName ??
49
+ `http-api-mock-${randomString()}`
50
+
51
+ const saveConfig = async () => {
52
+ writeFileSync(
53
+ path.join(process.cwd(), 'http-api-mock.json'),
54
+ JSON.stringify(
55
+ {
56
+ stackName,
57
+ ...(await stackOutput(new CloudFormationClient({}))<StackOutputs>(
58
+ stackName,
59
+ )),
60
+ },
61
+ null,
62
+ 2,
63
+ ),
64
+ )
65
+ }
66
+
67
+ if (options.config === true) {
68
+ await saveConfig()
69
+ process.exit(0)
70
+ }
71
+
72
+ const baseDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
73
+ const distDir = await fs.mkdtemp(path.join(os.tmpdir(), 'temp-'))
74
+ const lambdasDir = path.join(distDir, 'lambdas')
75
+ await fs.mkdir(lambdasDir)
76
+ const layersDir = path.join(distDir, 'layers')
77
+ await fs.mkdir(layersDir)
78
+
79
+ const dependencies: Array<keyof (typeof pJSON)['dependencies']> = [
80
+ '@bifravst/from-env',
81
+ ]
82
+
83
+ const app = new HTTPAPIMockApp(stackName, {
84
+ lambdaSources: {
85
+ httpApiMock: await packLambdaFromPath({
86
+ id: 'httpApiMock',
87
+ sourceFilePath: 'cdk/resources/http-api-mock-lambda.ts',
88
+ baseDir,
89
+ distDir: lambdasDir,
90
+ }),
91
+ },
92
+ layer: await packLayer({
93
+ id: 'testResources',
94
+ dependencies,
95
+ baseDir,
96
+ distDir: layersDir,
97
+ }),
98
+ })
99
+
100
+ const cdk = new Toolkit()
101
+
102
+ const cx = await cdk.fromAssemblyBuilder(async () => app.synth())
103
+
104
+ if (options.destroy === true) {
105
+ await cdk.destroy(cx)
106
+ } else {
107
+ await cdk.deploy(cx)
108
+ await saveConfig()
109
+ }
@@ -0,0 +1,117 @@
1
+ import type { PackedLambda } from '@bifravst/aws-cdk-lambda-helpers'
2
+ import {
3
+ LambdaLogGroup,
4
+ LambdaSource,
5
+ } from '@bifravst/aws-cdk-lambda-helpers/cdk'
6
+ import {
7
+ aws_apigateway as ApiGateway,
8
+ Duration,
9
+ aws_dynamodb as DynamoDB,
10
+ aws_iam as IAM,
11
+ aws_lambda as Lambda,
12
+ aws_logs as Logs,
13
+ RemovalPolicy,
14
+ Resource,
15
+ } from 'aws-cdk-lib'
16
+ import type { Construct } from 'constructs'
17
+
18
+ export class HttpApiMock extends Resource {
19
+ public readonly api: ApiGateway.RestApi
20
+ public readonly requestsTable: DynamoDB.Table
21
+ public readonly responsesTable: DynamoDB.Table
22
+
23
+ public constructor(
24
+ parent: Construct,
25
+ {
26
+ lambdaSources,
27
+ layers,
28
+ }: {
29
+ lambdaSources: {
30
+ httpApiMock: PackedLambda
31
+ }
32
+ layers: Lambda.ILayerVersion[]
33
+ },
34
+ ) {
35
+ super(parent, 'http-api-mock')
36
+
37
+ // This table will store all the requests made to the API Gateway
38
+ this.requestsTable = new DynamoDB.Table(this, 'requests', {
39
+ billingMode: DynamoDB.BillingMode.PAY_PER_REQUEST,
40
+ partitionKey: {
41
+ name: 'requestId',
42
+ type: DynamoDB.AttributeType.STRING,
43
+ },
44
+ sortKey: {
45
+ name: 'timestamp',
46
+ type: DynamoDB.AttributeType.STRING,
47
+ },
48
+ removalPolicy: RemovalPolicy.DESTROY,
49
+ })
50
+ this.requestsTable.addGlobalSecondaryIndex({
51
+ indexName: 'methodPathQuery',
52
+ partitionKey: {
53
+ name: 'methodPathQuery',
54
+ type: DynamoDB.AttributeType.STRING,
55
+ },
56
+ projectionType: DynamoDB.ProjectionType.ALL,
57
+ })
58
+
59
+ // This table will store optional responses to be sent
60
+ this.responsesTable = new DynamoDB.Table(this, 'responses', {
61
+ billingMode: DynamoDB.BillingMode.PAY_PER_REQUEST,
62
+ partitionKey: {
63
+ name: 'responseId',
64
+ type: DynamoDB.AttributeType.STRING,
65
+ },
66
+ sortKey: {
67
+ name: 'timestamp',
68
+ type: DynamoDB.AttributeType.STRING,
69
+ },
70
+ removalPolicy: RemovalPolicy.DESTROY,
71
+ timeToLiveAttribute: 'ttl',
72
+ })
73
+ this.responsesTable.addGlobalSecondaryIndex({
74
+ indexName: 'methodPathQuery',
75
+ partitionKey: {
76
+ name: 'methodPathQuery',
77
+ type: DynamoDB.AttributeType.STRING,
78
+ },
79
+ projectionType: DynamoDB.ProjectionType.ALL,
80
+ })
81
+
82
+ // This lambda will publish all requests made to the API Gateway in the queue
83
+ const lambda = new Lambda.Function(this, 'Lambda', {
84
+ description:
85
+ 'Mocks a HTTP API and stores all requests in SQS for inspection, and optionally replies with enqued responses',
86
+ code: new LambdaSource(this, lambdaSources.httpApiMock).code,
87
+ layers,
88
+ handler: lambdaSources.httpApiMock.handler,
89
+ architecture: Lambda.Architecture.ARM_64,
90
+ runtime: Lambda.Runtime.NODEJS_24_X,
91
+ timeout: Duration.seconds(5),
92
+ environment: {
93
+ REQUESTS_TABLE_NAME: this.requestsTable.tableName,
94
+ RESPONSES_TABLE_NAME: this.responsesTable.tableName,
95
+ LOG_LEVEL: this.node.tryGetContext('logLevel'),
96
+ NODE_NO_WARNINGS: '1',
97
+ },
98
+ ...new LambdaLogGroup(this, 'LambdaLogs', Logs.RetentionDays.ONE_DAY),
99
+ })
100
+ this.responsesTable.grantReadWriteData(lambda)
101
+ this.requestsTable.grantReadWriteData(lambda)
102
+
103
+ // This is the API Gateway, AWS CDK automatically creates a prod stage and deployment
104
+ this.api = new ApiGateway.RestApi(this, 'api', {
105
+ restApiName: `HTTP Mock API for testing`,
106
+ description: 'API Gateway to test outgoing requests',
107
+ binaryMediaTypes: ['application/octet-stream'],
108
+ })
109
+ const proxyResource = this.api.root.addResource('{proxy+}')
110
+ proxyResource.addMethod('ANY', new ApiGateway.LambdaIntegration(lambda))
111
+ // API Gateway needs to be able to call the lambda
112
+ lambda.addPermission('InvokeByApiGateway', {
113
+ principal: new IAM.ServicePrincipal('apigateway.amazonaws.com'),
114
+ sourceArn: this.api.arnForExecuteApi(),
115
+ })
116
+ }
117
+ }
@@ -0,0 +1,67 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+ import { checkMatchingQueryParams } from './checkMatchingQueryParams.ts'
4
+
5
+ void describe('checkMatchingQueryParams', () => {
6
+ void it('should return true when expected is subset of actual parameters', () => {
7
+ const actual = {
8
+ param1: 'value1',
9
+ param2: 'value2',
10
+ }
11
+ const expected = {
12
+ param1: 'value1',
13
+ }
14
+
15
+ const result = checkMatchingQueryParams(actual, expected)
16
+ assert.equal(result, true)
17
+ })
18
+
19
+ void it('should return true when expected contains regular expression and it matches', () => {
20
+ const actual = {
21
+ param1: 'value1,value2,value3',
22
+ }
23
+ const expected = {
24
+ param1: '/\\bvalue2\\b/',
25
+ }
26
+
27
+ const result = checkMatchingQueryParams(actual, expected)
28
+ assert.equal(result, true)
29
+ })
30
+
31
+ void it('should return false when expected does not match actual parameters', () => {
32
+ const actual = {
33
+ param1: 'value1',
34
+ param2: 'value2',
35
+ }
36
+ const expected = {
37
+ param1: 'value2',
38
+ }
39
+
40
+ const result = checkMatchingQueryParams(actual, expected)
41
+ assert.equal(result, false)
42
+ })
43
+
44
+ void it('should return false when actual is null', () => {
45
+ const actual = null
46
+ const expected = {
47
+ param1: 'value1',
48
+ }
49
+
50
+ const result = checkMatchingQueryParams(actual, expected)
51
+ assert.equal(result, false)
52
+ })
53
+
54
+ void it('should return true when expected parameters having number or boolean', () => {
55
+ const actual = {
56
+ param1: 'true',
57
+ param2: '1',
58
+ }
59
+ const expected = {
60
+ param1: true,
61
+ param2: 1,
62
+ }
63
+
64
+ const result = checkMatchingQueryParams(actual, expected)
65
+ assert.equal(result, true)
66
+ })
67
+ })
@@ -0,0 +1,40 @@
1
+ type Logger = {
2
+ debug: (...arg: any) => void
3
+ }
4
+ const matchRegex = /^\/(?<re>.+)\/(?<option>[gi])?$/
5
+
6
+ export const checkMatchingQueryParams = (
7
+ actual: Record<string, unknown> | null,
8
+ expected: Record<string, unknown>,
9
+ log?: Logger,
10
+ ): boolean => {
11
+ log?.debug('checkMatchingQueryParams', { actual, expected })
12
+ if (actual === null) return false
13
+
14
+ // Check whether expected query parameters is subset of actual query parameters
15
+ for (const prop in expected) {
16
+ const expectedValue = expected[prop]
17
+ const actualValue = actual?.[prop]
18
+ if (actualValue === undefined) return false
19
+
20
+ if (typeof expectedValue === 'string') {
21
+ const match = matchRegex.exec(expectedValue)
22
+ if (match !== null) {
23
+ log?.debug('Compare using regex', { expectedValue })
24
+ // Expect is regex
25
+ const check = new RegExp(
26
+ match?.groups?.re ?? '',
27
+ match?.groups?.option,
28
+ ).test(String(actualValue))
29
+ if (check === false) return false
30
+ } else {
31
+ if (actualValue !== expectedValue) return false
32
+ }
33
+ } else {
34
+ // All query parameters are string
35
+ if (actualValue !== String(expectedValue)) return false
36
+ }
37
+ }
38
+
39
+ return true
40
+ }
@@ -0,0 +1,147 @@
1
+ import {
2
+ DeleteItemCommand,
3
+ DynamoDBClient,
4
+ PutItemCommand,
5
+ ScanCommand,
6
+ } from '@aws-sdk/client-dynamodb'
7
+ import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'
8
+ import type {
9
+ APIGatewayEvent,
10
+ APIGatewayProxyResult,
11
+ Context,
12
+ } from 'aws-lambda'
13
+ import { URLSearchParams } from 'url'
14
+ import { sortQueryString } from '../../src/sortQueryString.ts'
15
+ import { checkMatchingQueryParams } from './checkMatchingQueryParams.ts'
16
+ import { splitMockResponse } from './splitMockResponse.ts'
17
+
18
+ const db = new DynamoDBClient({})
19
+
20
+ export const handler = async (
21
+ event: APIGatewayEvent,
22
+ context: Context,
23
+ ): Promise<APIGatewayProxyResult> => {
24
+ console.log(JSON.stringify({ event }))
25
+ const query =
26
+ event.queryStringParameters !== null &&
27
+ event.queryStringParameters !== undefined
28
+ ? new URLSearchParams(
29
+ event.queryStringParameters as Record<string, string>,
30
+ )
31
+ : undefined
32
+ const path = event.path.replace(/^\//, '')
33
+ const pathWithQuery = sortQueryString(
34
+ `${path}${query !== undefined ? `?${query.toString()}` : ''}`,
35
+ )
36
+
37
+ const request = {
38
+ methodPathQuery: `${event.httpMethod} ${pathWithQuery}`,
39
+ timestamp: new Date().toISOString(),
40
+ requestId: context.awsRequestId,
41
+ method: event.httpMethod,
42
+ path,
43
+ query: query === undefined ? null : Object.fromEntries(query),
44
+ body: event.body ?? '{}',
45
+ headers: JSON.stringify(event.headers),
46
+ }
47
+
48
+ // Check if response exists
49
+ console.debug(
50
+ `Checking if response exists for ${event.httpMethod} ${pathWithQuery}...`,
51
+ )
52
+ // Scan using httpMethod and path only so query strings can be partially matched
53
+ const { Items } = await db.send(
54
+ new ScanCommand({
55
+ TableName: process.env.RESPONSES_TABLE_NAME,
56
+ FilterExpression: 'begins_with(methodPathQuery, :methodPath)',
57
+ ExpressionAttributeValues: {
58
+ [':methodPath']: {
59
+ S: `${event.httpMethod} ${path}`,
60
+ },
61
+ },
62
+ }),
63
+ )
64
+ console.debug(
65
+ `Found response items beginning with same path: ${Items?.length}`,
66
+ )
67
+ // use newest response first
68
+ const itemsByTimestampDesc = (Items ?? [])
69
+ .map((Item) => unmarshall(Item))
70
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
71
+
72
+ let res: APIGatewayProxyResult = {
73
+ statusCode: 404,
74
+ body: 'No responses found',
75
+ }
76
+ for (const objItem of itemsByTimestampDesc) {
77
+ const hasExpectedQueryParams =
78
+ 'queryParams' in objItem || query !== undefined
79
+ const matchedQueryParams = hasExpectedQueryParams
80
+ ? checkMatchingQueryParams(
81
+ event.queryStringParameters,
82
+ objItem.queryParams,
83
+ )
84
+ : true
85
+ if (matchedQueryParams === false) continue
86
+
87
+ console.debug(`Matched response`, JSON.stringify({ response: objItem }))
88
+
89
+ if (
90
+ objItem?.requestId !== undefined &&
91
+ objItem?.timestamp !== undefined &&
92
+ objItem?.keep !== true
93
+ ) {
94
+ await db.send(
95
+ new DeleteItemCommand({
96
+ TableName: process.env.RESPONSES_TABLE_NAME,
97
+ Key: marshall({
98
+ requestId: objItem.requestId,
99
+ timestamp: objItem.timestamp,
100
+ }),
101
+ }),
102
+ )
103
+ }
104
+
105
+ const { body, headers } = splitMockResponse(objItem.body ?? '')
106
+
107
+ // Send as binary, if mock response is HEX encoded. See https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings.html
108
+ const isBinary = /^[0-9a-f]+$/.test(body)
109
+ res = {
110
+ statusCode: objItem.statusCode ?? 200,
111
+ headers: isBinary
112
+ ? {
113
+ ...headers,
114
+ 'Content-Type': 'application/octet-stream',
115
+ }
116
+ : headers,
117
+ body: isBinary
118
+ ? /* body is HEX encoded */ Buffer.from(body, 'hex').toString('base64')
119
+ : body,
120
+ isBase64Encoded: isBinary,
121
+ }
122
+
123
+ break
124
+ }
125
+
126
+ console.debug(`Return response`, JSON.stringify({ response: res }))
127
+
128
+ await db.send(
129
+ new PutItemCommand({
130
+ TableName: process.env.REQUESTS_TABLE_NAME,
131
+ Item: marshall(
132
+ {
133
+ ...request,
134
+ responseStatusCode: res.statusCode,
135
+ responseHeaders: res.headers,
136
+ responseBody: res.body,
137
+ responseIsBase64Encoded: res.isBase64Encoded,
138
+ },
139
+ {
140
+ removeUndefinedValues: true,
141
+ },
142
+ ),
143
+ }),
144
+ )
145
+
146
+ return res
147
+ }
@@ -0,0 +1,17 @@
1
+ import assert from 'node:assert'
2
+ import { describe, it } from 'node:test'
3
+ import { splitMockResponse } from './splitMockResponse.ts'
4
+ void describe('split mock response', () => {
5
+ void it('should parse headers and body', () =>
6
+ assert.deepEqual(
7
+ splitMockResponse(`Content-Type: application/octet-stream
8
+
9
+ (binary A-GNSS data) other types`),
10
+ {
11
+ headers: {
12
+ 'Content-Type': 'application/octet-stream',
13
+ },
14
+ body: '(binary A-GNSS data) other types',
15
+ },
16
+ ))
17
+ })
@@ -0,0 +1,25 @@
1
+ export const splitMockResponse = (
2
+ r: string,
3
+ ): { headers: Record<string, string>; body: string } => {
4
+ const trimmedLines = r
5
+ .split('\n')
6
+ .map((s) => s.trim())
7
+ .join('\n')
8
+ const blankLineLocation = trimmedLines.indexOf('\n\n')
9
+ if (blankLineLocation === -1)
10
+ return {
11
+ headers: {},
12
+ body: trimmedLines,
13
+ }
14
+ return {
15
+ headers: trimmedLines
16
+ .slice(0, blankLineLocation)
17
+ .split('\n')
18
+ .map((s) => s.split(':', 2))
19
+ .reduce(
20
+ (headers, [k, v]) => ({ ...headers, [k as string]: v?.trim() }),
21
+ {},
22
+ ),
23
+ body: trimmedLines.slice(blankLineLocation + 2),
24
+ }
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bifravst/http-api-mock",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "Helper functions for AWS lambdas written in TypeScript.",
5
5
  "exports": {
6
6
  "./*": {
@@ -13,7 +13,7 @@
13
13
  "type": "module",
14
14
  "scripts": {
15
15
  "prepare": "husky && case \"$npm_command\" in install|ci) check-node-version --package ;; esac",
16
- "prepublishOnly": "node --no-warnings --experimental-transform-types ./.npm/compile.ts",
16
+ "prepublishOnly": "node --no-warnings --experimental-transform-types ./.npm/compile.ts && cp -rv cdk/* npm/cdk",
17
17
  "test": "node --no-warnings --experimental-transform-types --test \"!(node_modules|e2e-tests|integration-tests)/**/*.spec.ts\""
18
18
  },
19
19
  "repository": {
@@ -77,6 +77,7 @@
77
77
  "@aws-sdk/client-cloudformation": "3.1111.0",
78
78
  "@aws-sdk/client-dynamodb": "3.1111.0",
79
79
  "@aws-sdk/client-sts": "3.1111.0",
80
+ "@aws-cdk/toolkit-lib": "1.38.2",
80
81
  "@aws-sdk/util-dynamodb": "3.996.9",
81
82
  "@bifravst/aws-cdk-lambda-helpers": "4.0.96",
82
83
  "@bifravst/cloudformation-helpers": "9.1.1",
@@ -84,10 +85,10 @@
84
85
  "@bifravst/run": "1.2.0",
85
86
  "aws-cdk-lib": "2.265.0",
86
87
  "cdk": "2.1136.0",
87
- "chalk": "6.0.0"
88
+ "chalk": "6.0.0",
89
+ "command-line-args": "6.0.2"
88
90
  },
89
91
  "devDependencies": {
90
- "@aws-cdk/toolkit-lib": "1.38.2",
91
92
  "@bifravst/eslint-config-typescript": "7.0.34",
92
93
  "@bifravst/prettier-config": "1.1.17",
93
94
  "@commitlint/config-conventional": "21.2.2",
@@ -96,7 +97,6 @@
96
97
  "@types/node": "26.2.0",
97
98
  "@typescript/native": "npm:typescript@7.0.2",
98
99
  "check-node-version": "4.2.1",
99
- "command-line-args": "6.0.2",
100
100
  "commitlint": "21.2.2",
101
101
  "husky": "9.1.7",
102
102
  "lint-staged": "17.3.0",