@cxbuilder/flow-config 1.0.2 → 1.1.0
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/.jsii +43 -32
- package/CHANGELOG.md +18 -0
- package/dist/backend/FlowConfig/index.js +109 -15
- package/dist/backend/FlowConfig/index.js.map +3 -3
- package/dist/backend/Static/static/assets/{index-Bx9Z3cF9.js → index-FHwnAA8f.js} +14 -14
- package/dist/backend/Static/static/index.html +1 -1
- package/dist/infrastructure/FlowConfigStack.d.ts +4 -0
- package/dist/infrastructure/FlowConfigStack.js +29 -2
- package/dist/infrastructure/tsconfig.tsbuildinfo +1 -1
- package/docs/Permissions-v1.md +132 -0
- package/docs/{Permissions.md → Permissions-v2.md} +15 -15
- package/package.json +1 -1
package/.jsii
CHANGED
|
@@ -4016,7 +4016,7 @@
|
|
|
4016
4016
|
"kind": "interface",
|
|
4017
4017
|
"locationInModule": {
|
|
4018
4018
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4019
|
-
"line":
|
|
4019
|
+
"line": 24
|
|
4020
4020
|
},
|
|
4021
4021
|
"name": "CognitoConfig",
|
|
4022
4022
|
"properties": [
|
|
@@ -4029,7 +4029,7 @@
|
|
|
4029
4029
|
"immutable": true,
|
|
4030
4030
|
"locationInModule": {
|
|
4031
4031
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4032
|
-
"line":
|
|
4032
|
+
"line": 30
|
|
4033
4033
|
},
|
|
4034
4034
|
"name": "domain",
|
|
4035
4035
|
"type": {
|
|
@@ -4044,7 +4044,7 @@
|
|
|
4044
4044
|
"immutable": true,
|
|
4045
4045
|
"locationInModule": {
|
|
4046
4046
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4047
|
-
"line":
|
|
4047
|
+
"line": 25
|
|
4048
4048
|
},
|
|
4049
4049
|
"name": "userPoolId",
|
|
4050
4050
|
"type": {
|
|
@@ -4061,7 +4061,7 @@
|
|
|
4061
4061
|
"immutable": true,
|
|
4062
4062
|
"locationInModule": {
|
|
4063
4063
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4064
|
-
"line":
|
|
4064
|
+
"line": 35
|
|
4065
4065
|
},
|
|
4066
4066
|
"name": "ssoProviderName",
|
|
4067
4067
|
"optional": true,
|
|
@@ -4085,7 +4085,7 @@
|
|
|
4085
4085
|
},
|
|
4086
4086
|
"locationInModule": {
|
|
4087
4087
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4088
|
-
"line":
|
|
4088
|
+
"line": 143
|
|
4089
4089
|
},
|
|
4090
4090
|
"parameters": [
|
|
4091
4091
|
{
|
|
@@ -4111,7 +4111,7 @@
|
|
|
4111
4111
|
"kind": "class",
|
|
4112
4112
|
"locationInModule": {
|
|
4113
4113
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4114
|
-
"line":
|
|
4114
|
+
"line": 118
|
|
4115
4115
|
},
|
|
4116
4116
|
"methods": [
|
|
4117
4117
|
{
|
|
@@ -4121,7 +4121,7 @@
|
|
|
4121
4121
|
},
|
|
4122
4122
|
"locationInModule": {
|
|
4123
4123
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4124
|
-
"line":
|
|
4124
|
+
"line": 267
|
|
4125
4125
|
},
|
|
4126
4126
|
"name": "associate3pApp"
|
|
4127
4127
|
},
|
|
@@ -4131,7 +4131,7 @@
|
|
|
4131
4131
|
},
|
|
4132
4132
|
"locationInModule": {
|
|
4133
4133
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4134
|
-
"line":
|
|
4134
|
+
"line": 207
|
|
4135
4135
|
},
|
|
4136
4136
|
"name": "createUserPoolClient",
|
|
4137
4137
|
"returns": {
|
|
@@ -4139,6 +4139,17 @@
|
|
|
4139
4139
|
"fqn": "aws-cdk-lib.aws_cognito.UserPoolClient"
|
|
4140
4140
|
}
|
|
4141
4141
|
}
|
|
4142
|
+
},
|
|
4143
|
+
{
|
|
4144
|
+
"docs": {
|
|
4145
|
+
"stability": "stable",
|
|
4146
|
+
"summary": "Create Cognito User Groups for role-based access control."
|
|
4147
|
+
},
|
|
4148
|
+
"locationInModule": {
|
|
4149
|
+
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4150
|
+
"line": 235
|
|
4151
|
+
},
|
|
4152
|
+
"name": "createUserPoolGroups"
|
|
4142
4153
|
}
|
|
4143
4154
|
],
|
|
4144
4155
|
"name": "FlowConfigStack",
|
|
@@ -4150,7 +4161,7 @@
|
|
|
4150
4161
|
"immutable": true,
|
|
4151
4162
|
"locationInModule": {
|
|
4152
4163
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4153
|
-
"line":
|
|
4164
|
+
"line": 138
|
|
4154
4165
|
},
|
|
4155
4166
|
"name": "appUrl",
|
|
4156
4167
|
"type": {
|
|
@@ -4163,7 +4174,7 @@
|
|
|
4163
4174
|
},
|
|
4164
4175
|
"locationInModule": {
|
|
4165
4176
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4166
|
-
"line":
|
|
4177
|
+
"line": 122
|
|
4167
4178
|
},
|
|
4168
4179
|
"name": "alertTopic",
|
|
4169
4180
|
"type": {
|
|
@@ -4176,7 +4187,7 @@
|
|
|
4176
4187
|
},
|
|
4177
4188
|
"locationInModule": {
|
|
4178
4189
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4179
|
-
"line":
|
|
4190
|
+
"line": 146
|
|
4180
4191
|
},
|
|
4181
4192
|
"name": "props",
|
|
4182
4193
|
"type": {
|
|
@@ -4189,7 +4200,7 @@
|
|
|
4189
4200
|
},
|
|
4190
4201
|
"locationInModule": {
|
|
4191
4202
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4192
|
-
"line":
|
|
4203
|
+
"line": 123
|
|
4193
4204
|
},
|
|
4194
4205
|
"name": "table",
|
|
4195
4206
|
"type": {
|
|
@@ -4202,7 +4213,7 @@
|
|
|
4202
4213
|
},
|
|
4203
4214
|
"locationInModule": {
|
|
4204
4215
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4205
|
-
"line":
|
|
4216
|
+
"line": 119
|
|
4206
4217
|
},
|
|
4207
4218
|
"name": "userPool",
|
|
4208
4219
|
"type": {
|
|
@@ -4215,7 +4226,7 @@
|
|
|
4215
4226
|
},
|
|
4216
4227
|
"locationInModule": {
|
|
4217
4228
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4218
|
-
"line":
|
|
4229
|
+
"line": 121
|
|
4219
4230
|
},
|
|
4220
4231
|
"name": "userPoolClient",
|
|
4221
4232
|
"type": {
|
|
@@ -4238,7 +4249,7 @@
|
|
|
4238
4249
|
"kind": "interface",
|
|
4239
4250
|
"locationInModule": {
|
|
4240
4251
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4241
|
-
"line":
|
|
4252
|
+
"line": 89
|
|
4242
4253
|
},
|
|
4243
4254
|
"name": "FlowConfigStackProps",
|
|
4244
4255
|
"properties": [
|
|
@@ -4251,7 +4262,7 @@
|
|
|
4251
4262
|
"immutable": true,
|
|
4252
4263
|
"locationInModule": {
|
|
4253
4264
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4254
|
-
"line":
|
|
4265
|
+
"line": 100
|
|
4255
4266
|
},
|
|
4256
4267
|
"name": "alertEmails",
|
|
4257
4268
|
"type": {
|
|
@@ -4271,7 +4282,7 @@
|
|
|
4271
4282
|
"immutable": true,
|
|
4272
4283
|
"locationInModule": {
|
|
4273
4284
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4274
|
-
"line":
|
|
4285
|
+
"line": 94
|
|
4275
4286
|
},
|
|
4276
4287
|
"name": "cognito",
|
|
4277
4288
|
"type": {
|
|
@@ -4286,7 +4297,7 @@
|
|
|
4286
4297
|
"immutable": true,
|
|
4287
4298
|
"locationInModule": {
|
|
4288
4299
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4289
|
-
"line":
|
|
4300
|
+
"line": 95
|
|
4290
4301
|
},
|
|
4291
4302
|
"name": "connectInstanceArn",
|
|
4292
4303
|
"type": {
|
|
@@ -4302,7 +4313,7 @@
|
|
|
4302
4313
|
"immutable": true,
|
|
4303
4314
|
"locationInModule": {
|
|
4304
4315
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4305
|
-
"line":
|
|
4316
|
+
"line": 93
|
|
4306
4317
|
},
|
|
4307
4318
|
"name": "prefix",
|
|
4308
4319
|
"type": {
|
|
@@ -4319,7 +4330,7 @@
|
|
|
4319
4330
|
"immutable": true,
|
|
4320
4331
|
"locationInModule": {
|
|
4321
4332
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4322
|
-
"line":
|
|
4333
|
+
"line": 115
|
|
4323
4334
|
},
|
|
4324
4335
|
"name": "globalTable",
|
|
4325
4336
|
"optional": true,
|
|
@@ -4335,7 +4346,7 @@
|
|
|
4335
4346
|
"immutable": true,
|
|
4336
4347
|
"locationInModule": {
|
|
4337
4348
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4338
|
-
"line":
|
|
4349
|
+
"line": 101
|
|
4339
4350
|
},
|
|
4340
4351
|
"name": "prod",
|
|
4341
4352
|
"optional": true,
|
|
@@ -4353,7 +4364,7 @@
|
|
|
4353
4364
|
"immutable": true,
|
|
4354
4365
|
"locationInModule": {
|
|
4355
4366
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4356
|
-
"line":
|
|
4367
|
+
"line": 108
|
|
4357
4368
|
},
|
|
4358
4369
|
"name": "vpc",
|
|
4359
4370
|
"optional": true,
|
|
@@ -4375,7 +4386,7 @@
|
|
|
4375
4386
|
"kind": "interface",
|
|
4376
4387
|
"locationInModule": {
|
|
4377
4388
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4378
|
-
"line":
|
|
4389
|
+
"line": 66
|
|
4379
4390
|
},
|
|
4380
4391
|
"name": "GlobalTableConfig",
|
|
4381
4392
|
"properties": [
|
|
@@ -4388,7 +4399,7 @@
|
|
|
4388
4399
|
"immutable": true,
|
|
4389
4400
|
"locationInModule": {
|
|
4390
4401
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4391
|
-
"line":
|
|
4402
|
+
"line": 70
|
|
4392
4403
|
},
|
|
4393
4404
|
"name": "isPrimaryRegion",
|
|
4394
4405
|
"type": {
|
|
@@ -4404,7 +4415,7 @@
|
|
|
4404
4415
|
"immutable": true,
|
|
4405
4416
|
"locationInModule": {
|
|
4406
4417
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4407
|
-
"line":
|
|
4418
|
+
"line": 76
|
|
4408
4419
|
},
|
|
4409
4420
|
"name": "replicaRegions",
|
|
4410
4421
|
"optional": true,
|
|
@@ -4431,7 +4442,7 @@
|
|
|
4431
4442
|
"kind": "interface",
|
|
4432
4443
|
"locationInModule": {
|
|
4433
4444
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4434
|
-
"line":
|
|
4445
|
+
"line": 41
|
|
4435
4446
|
},
|
|
4436
4447
|
"name": "VpcConfig",
|
|
4437
4448
|
"properties": [
|
|
@@ -4444,7 +4455,7 @@
|
|
|
4444
4455
|
"immutable": true,
|
|
4445
4456
|
"locationInModule": {
|
|
4446
4457
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4447
|
-
"line":
|
|
4458
|
+
"line": 50
|
|
4448
4459
|
},
|
|
4449
4460
|
"name": "lambdaSecurityGroupIds",
|
|
4450
4461
|
"type": {
|
|
@@ -4465,7 +4476,7 @@
|
|
|
4465
4476
|
"immutable": true,
|
|
4466
4477
|
"locationInModule": {
|
|
4467
4478
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4468
|
-
"line":
|
|
4479
|
+
"line": 55
|
|
4469
4480
|
},
|
|
4470
4481
|
"name": "privateSubnetIds",
|
|
4471
4482
|
"type": {
|
|
@@ -4486,7 +4497,7 @@
|
|
|
4486
4497
|
"immutable": true,
|
|
4487
4498
|
"locationInModule": {
|
|
4488
4499
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4489
|
-
"line":
|
|
4500
|
+
"line": 60
|
|
4490
4501
|
},
|
|
4491
4502
|
"name": "vpcEndpointSecurityGroupIds",
|
|
4492
4503
|
"type": {
|
|
@@ -4507,7 +4518,7 @@
|
|
|
4507
4518
|
"immutable": true,
|
|
4508
4519
|
"locationInModule": {
|
|
4509
4520
|
"filename": "infrastructure/FlowConfigStack.ts",
|
|
4510
|
-
"line":
|
|
4521
|
+
"line": 45
|
|
4511
4522
|
},
|
|
4512
4523
|
"name": "vpcId",
|
|
4513
4524
|
"type": {
|
|
@@ -4518,6 +4529,6 @@
|
|
|
4518
4529
|
"symbolId": "infrastructure/FlowConfigStack:VpcConfig"
|
|
4519
4530
|
}
|
|
4520
4531
|
},
|
|
4521
|
-
"version": "1.0
|
|
4522
|
-
"fingerprint": "
|
|
4532
|
+
"version": "1.1.0",
|
|
4533
|
+
"fingerprint": "HFZB1viUfyeGHk8/FgKUYwz6TaxogYZ+PsdAxRdPVdA="
|
|
4523
4534
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.0] - 2025-06-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Role-based access control (RBAC) using Amazon Cognito User Groups
|
|
13
|
+
- Three permission levels: FlowConfigAdmin, FlowConfigEdit, and FlowConfigRead
|
|
14
|
+
- Backend permission validation for all API endpoints
|
|
15
|
+
- Frontend UI adapts based on user permissions
|
|
16
|
+
- Read-only mode for users without edit access
|
|
17
|
+
- Access denied screen for users without any FlowConfig permissions
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Replaced placeholder permission system with full Cognito Groups implementation
|
|
22
|
+
- FlowConfigEdit users can add languages to prompts but cannot remove existing ones
|
|
23
|
+
- FlowConfigEdit users can add/remove channels but cannot modify structure
|
|
24
|
+
- Preview functionality remains available to all permission levels
|
|
25
|
+
|
|
8
26
|
## [1.0.2] - 2025-06-20
|
|
9
27
|
|
|
10
28
|
- Converted to `SpecRestApi` because `@aws-solutions-constructs/aws-openapigateway-lambda` is not compatible with `Role.customizeRoles`
|
|
@@ -151,6 +151,78 @@ function stripSSML(text) {
|
|
|
151
151
|
}
|
|
152
152
|
__name(stripSSML, "stripSSML");
|
|
153
153
|
|
|
154
|
+
// backend/shared/permissions.ts
|
|
155
|
+
var COGNITO_GROUPS = {
|
|
156
|
+
ADMIN: "FlowConfigAdmin",
|
|
157
|
+
EDIT: "FlowConfigEdit",
|
|
158
|
+
READ: "FlowConfigRead"
|
|
159
|
+
};
|
|
160
|
+
function extractCognitoGroups(claims) {
|
|
161
|
+
const groupsClaim = claims["cognito:groups"];
|
|
162
|
+
if (!groupsClaim) {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
if (typeof groupsClaim === "string") {
|
|
166
|
+
return groupsClaim.split(",").map((group) => group.trim());
|
|
167
|
+
}
|
|
168
|
+
if (Array.isArray(groupsClaim)) {
|
|
169
|
+
return groupsClaim;
|
|
170
|
+
}
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
__name(extractCognitoGroups, "extractCognitoGroups");
|
|
174
|
+
function hasFlowConfigAccess(claims) {
|
|
175
|
+
const userGroups = extractCognitoGroups(claims);
|
|
176
|
+
const flowConfigGroups = Object.values(COGNITO_GROUPS);
|
|
177
|
+
return userGroups.some((group) => flowConfigGroups.includes(group));
|
|
178
|
+
}
|
|
179
|
+
__name(hasFlowConfigAccess, "hasFlowConfigAccess");
|
|
180
|
+
function getAccessLevel(claims) {
|
|
181
|
+
const userGroups = extractCognitoGroups(claims);
|
|
182
|
+
if (userGroups.includes(COGNITO_GROUPS.ADMIN)) {
|
|
183
|
+
return "Full";
|
|
184
|
+
}
|
|
185
|
+
if (userGroups.includes(COGNITO_GROUPS.EDIT)) {
|
|
186
|
+
return "Edit";
|
|
187
|
+
}
|
|
188
|
+
if (userGroups.includes(COGNITO_GROUPS.READ)) {
|
|
189
|
+
return "Read";
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
__name(getAccessLevel, "getAccessLevel");
|
|
194
|
+
function checkActionPermission(claims, action) {
|
|
195
|
+
const accessLevel = getAccessLevel(claims);
|
|
196
|
+
if (!accessLevel) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
switch (action) {
|
|
200
|
+
case "Read":
|
|
201
|
+
return accessLevel;
|
|
202
|
+
case "Edit":
|
|
203
|
+
if (accessLevel === "Edit" || accessLevel === "Full") {
|
|
204
|
+
return accessLevel;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
case "Create":
|
|
208
|
+
case "Delete":
|
|
209
|
+
if (accessLevel === "Full") {
|
|
210
|
+
return accessLevel;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
default:
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
__name(checkActionPermission, "checkActionPermission");
|
|
218
|
+
function validateFlowConfigPermission(claims, _flowConfigId, action) {
|
|
219
|
+
if (!hasFlowConfigAccess(claims)) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
return checkActionPermission(claims, action);
|
|
223
|
+
}
|
|
224
|
+
__name(validateFlowConfigPermission, "validateFlowConfigPermission");
|
|
225
|
+
|
|
154
226
|
// backend/FlowConfig.ts
|
|
155
227
|
var env = process.env;
|
|
156
228
|
var client2 = new import_client_dynamodb.DynamoDBClient();
|
|
@@ -198,7 +270,7 @@ async function listFlowConfigs(event, claims) {
|
|
|
198
270
|
}
|
|
199
271
|
const resultItems = [];
|
|
200
272
|
for (const config of filteredConfigs) {
|
|
201
|
-
const accessLevel =
|
|
273
|
+
const accessLevel = validateFlowConfigPermission(claims, config.id, "Read");
|
|
202
274
|
if (accessLevel) {
|
|
203
275
|
resultItems.push({
|
|
204
276
|
id: config.id,
|
|
@@ -216,7 +288,7 @@ async function listFlowConfigs(event, claims) {
|
|
|
216
288
|
__name(listFlowConfigs, "listFlowConfigs");
|
|
217
289
|
async function getFlowConfig(flowConfigId, claims) {
|
|
218
290
|
try {
|
|
219
|
-
const accessLevel =
|
|
291
|
+
const accessLevel = validateFlowConfigPermission(claims, flowConfigId, "Read");
|
|
220
292
|
if (!accessLevel) {
|
|
221
293
|
return respondMessage(403, "Access denied");
|
|
222
294
|
}
|
|
@@ -278,10 +350,16 @@ async function saveFlowConfig(flowConfigId, event, claims) {
|
|
|
278
350
|
const response = await docClient.send(getCommand);
|
|
279
351
|
const existingConfig = response.Item;
|
|
280
352
|
const action = existingConfig ? "Edit" : "Create";
|
|
281
|
-
const accessLevel =
|
|
353
|
+
const accessLevel = validateFlowConfigPermission(claims, flowConfigId, action);
|
|
282
354
|
if (!accessLevel) {
|
|
283
355
|
return respondMessage(403, "Access denied");
|
|
284
356
|
}
|
|
357
|
+
if (accessLevel === "Edit" && existingConfig) {
|
|
358
|
+
const structuralChangeError = validateEditOnlyChanges(existingConfig, body);
|
|
359
|
+
if (structuralChangeError) {
|
|
360
|
+
return respondMessage(403, `FlowConfigEdit users cannot make structural changes: ${structuralChangeError}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
285
363
|
const putCommand = new import_lib_dynamodb.PutCommand({
|
|
286
364
|
TableName: env.FLOW_CONFIGS_TABLE_NAME,
|
|
287
365
|
Item: body
|
|
@@ -296,7 +374,7 @@ async function saveFlowConfig(flowConfigId, event, claims) {
|
|
|
296
374
|
__name(saveFlowConfig, "saveFlowConfig");
|
|
297
375
|
async function deleteFlowConfig(flowConfigId, claims) {
|
|
298
376
|
try {
|
|
299
|
-
const accessLevel =
|
|
377
|
+
const accessLevel = validateFlowConfigPermission(claims, flowConfigId, "Delete");
|
|
300
378
|
if (!accessLevel) {
|
|
301
379
|
return respondMessage(403, "Access denied");
|
|
302
380
|
}
|
|
@@ -355,7 +433,7 @@ async function previewFlowConfig(event, claims) {
|
|
|
355
433
|
"FlowConfig must have id, description, variables, and prompts"
|
|
356
434
|
);
|
|
357
435
|
}
|
|
358
|
-
const accessLevel =
|
|
436
|
+
const accessLevel = validateFlowConfigPermission(claims, flowConfig.id, "Read");
|
|
359
437
|
if (!accessLevel) {
|
|
360
438
|
return respondMessage(403, "Access denied");
|
|
361
439
|
}
|
|
@@ -369,18 +447,34 @@ async function previewFlowConfig(event, claims) {
|
|
|
369
447
|
}
|
|
370
448
|
}
|
|
371
449
|
__name(previewFlowConfig, "previewFlowConfig");
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
return
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
450
|
+
function validateEditOnlyChanges(existingConfig, newConfig) {
|
|
451
|
+
if (existingConfig.description !== newConfig.description) {
|
|
452
|
+
return "Cannot modify description";
|
|
453
|
+
}
|
|
454
|
+
const existingVarKeys = Object.keys(existingConfig.variables || {}).sort();
|
|
455
|
+
const newVarKeys = Object.keys(newConfig.variables || {}).sort();
|
|
456
|
+
if (existingVarKeys.length !== newVarKeys.length || !existingVarKeys.every((key, index) => key === newVarKeys[index])) {
|
|
457
|
+
return "Cannot add or remove variables";
|
|
458
|
+
}
|
|
459
|
+
const existingPromptKeys = Object.keys(existingConfig.prompts || {}).sort();
|
|
460
|
+
const newPromptKeys = Object.keys(newConfig.prompts || {}).sort();
|
|
461
|
+
if (existingPromptKeys.length !== newPromptKeys.length || !existingPromptKeys.every((key, index) => key === newPromptKeys[index])) {
|
|
462
|
+
return "Cannot add or remove prompts";
|
|
463
|
+
}
|
|
464
|
+
for (const promptName of existingPromptKeys) {
|
|
465
|
+
const existingPrompt = existingConfig.prompts[promptName];
|
|
466
|
+
const newPrompt = newConfig.prompts[promptName];
|
|
467
|
+
const existingLangs = Object.keys(existingPrompt || {});
|
|
468
|
+
const newLangs = Object.keys(newPrompt || {});
|
|
469
|
+
for (const existingLang of existingLangs) {
|
|
470
|
+
if (!newLangs.includes(existingLang)) {
|
|
471
|
+
return `Cannot remove language ${existingLang} from prompt ${promptName}`;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
381
474
|
}
|
|
475
|
+
return null;
|
|
382
476
|
}
|
|
383
|
-
__name(
|
|
477
|
+
__name(validateEditOnlyChanges, "validateEditOnlyChanges");
|
|
384
478
|
// Annotate the CommonJS export names for ESM import in node:
|
|
385
479
|
0 && (module.exports = {
|
|
386
480
|
handler
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../../backend/FlowConfig.ts", "../../../backend/shared/logger.ts", "../../../backend/shared/respond.ts", "../../../backend/shared/snsClient.ts", "../../../backend/shared/getVar.ts", "../../../backend/shared/transformFlowConfig.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n APIGatewayProxyEvent,\n APIGatewayProxyStructuredResultV2,\n Context,\n} from 'aws-lambda';\nimport { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport {\n DynamoDBDocumentClient,\n GetCommand,\n ScanCommand,\n PutCommand,\n DeleteCommand,\n} from '@aws-sdk/lib-dynamodb';\nimport { logEvent } from './shared/logger';\nimport { FlowConfigEnv } from '../infrastructure/api/FlowConfig/FlowConfig.interface';\nimport { respondError, respondMessage, respondObject } from './shared/respond';\nimport { sendError } from './shared/snsClient';\nimport { transformFlowConfig } from './shared/transformFlowConfig';\n\nconst env = process.env as unknown as FlowConfigEnv;\nconst client = new DynamoDBClient();\nconst docClient = DynamoDBDocumentClient.from(client);\nimport { FlowConfig, FlowConfigList, FlowConfigSummary } from './shared/models';\nexport const handler = async (\n event: APIGatewayProxyEvent,\n context?: Context\n): Promise<APIGatewayProxyStructuredResultV2> => {\n logEvent(event, context);\n\n try {\n const method = event.httpMethod;\n const path = event.path;\n const pathParameters = event.pathParameters;\n\n // Extract user claims from Cognito authorizer\n const claims = event.requestContext.authorizer?.claims;\n\n if (!claims) {\n return respondObject(401, new Error('Unauthorized'));\n }\n\n // Route to appropriate handler\n if (method === 'GET' && path === '/api/flow-config') {\n return await listFlowConfigs(event, claims);\n } else if (method === 'GET' && pathParameters?.id) {\n return await getFlowConfig(pathParameters.id, claims);\n } else if (method === 'POST' && path === '/api/flow-config/preview') {\n return await previewFlowConfig(event, claims);\n } else if (method === 'POST' && pathParameters?.id) {\n return await saveFlowConfig(pathParameters.id, event, claims);\n } else if (method === 'DELETE' && pathParameters?.id) {\n return await deleteFlowConfig(pathParameters.id, claims);\n }\n\n return respondMessage(404, 'Not Found');\n } catch (error) {\n await sendError('Unhandled Error: api/flow-config', error as Error);\n return respondError(error);\n }\n};\n\nasync function listFlowConfigs(\n event: APIGatewayProxyEvent,\n claims: Record<string, string>\n): Promise<APIGatewayProxyStructuredResultV2> {\n try {\n // Get all flow configs from DynamoDB\n const scanCommand = new ScanCommand({\n TableName: env.FLOW_CONFIGS_TABLE_NAME,\n });\n\n const response = await docClient.send(scanCommand);\n const flowConfigs = response.Items || [];\n\n // Get query parameters for filtering\n const pattern = event.queryStringParameters?.pattern;\n\n // Filter by pattern if provided\n let filteredConfigs = flowConfigs;\n if (pattern) {\n filteredConfigs = flowConfigs.filter((config) =>\n config.id.startsWith(pattern)\n );\n }\n\n // Check permissions for each flow config\n const resultItems: FlowConfigSummary[] = [];\n\n for (const config of filteredConfigs) {\n // Check access level using user claims\n const accessLevel = await checkPermissions(claims, config.id, 'Read');\n if (accessLevel) {\n resultItems.push({\n id: config.id,\n description: config.description,\n accessLevel,\n });\n }\n }\n\n const result: FlowConfigList = { items: resultItems };\n return respondObject(200, result);\n } catch (error) {\n throw new Error(`Error listing flow configs: ${error}`);\n }\n}\n\nasync function getFlowConfig(\n flowConfigId: string,\n claims: Record<string, string>\n): Promise<APIGatewayProxyStructuredResultV2> {\n try {\n // Check permissions\n const accessLevel = await checkPermissions(claims, flowConfigId, 'Read');\n if (!accessLevel) {\n return respondMessage(403, 'Access denied');\n }\n\n // Get flow config from DynamoDB\n const getCommand = new GetCommand({\n TableName: env.FLOW_CONFIGS_TABLE_NAME,\n Key: { id: flowConfigId },\n });\n\n const response = await docClient.send(getCommand);\n\n if (!response.Item) {\n return respondMessage(404, 'Flow Config not found');\n }\n\n return respondObject(200, response.Item as FlowConfig);\n } catch (error) {\n throw new Error(`Error getting flow config ${flowConfigId}: ${error}`);\n }\n}\n\nasync function saveFlowConfig(\n flowConfigId: string,\n event: APIGatewayProxyEvent,\n claims: Record<string, string>\n): Promise<APIGatewayProxyStructuredResultV2> {\n // Parse and validate request body first (outside try-catch)\n if (!event.body) {\n return respondMessage(400, 'Request body required');\n }\n\n let body: FlowConfig;\n try {\n body = JSON.parse(event.body) as FlowConfig;\n } catch (error) {\n return respondMessage(400, 'Invalid JSON in request body');\n }\n\n // Ensure ID in body matches path parameter\n body.id = flowConfigId;\n\n // Basic validation\n if (!body.description || !body.variables || !body.prompts) {\n return respondMessage(\n 400,\n 'Missing required fields: description, variables, prompts'\n );\n }\n\n // Validate variables are strings\n for (const [key, value] of Object.entries(body.variables)) {\n if (typeof value !== 'string') {\n return respondMessage(400, `Variable ${key} must be a string`);\n }\n }\n\n // Validate prompts structure\n for (const [promptName, promptData] of Object.entries(body.prompts)) {\n for (const [lang, langData] of Object.entries(promptData)) {\n if (!langData.voice) {\n return respondMessage(\n 400,\n `Prompt ${promptName} for language ${lang} must have a voice variant`\n );\n }\n }\n }\n\n // Check size constraints (approximate)\n const itemSize = JSON.stringify(body).length;\n if (itemSize > 380000) {\n // Leave some buffer for DynamoDB 400KB limit\n return respondMessage(413, 'Flow config exceeds maximum size limit');\n }\n\n try {\n // Check if flow config exists\n const getCommand = new GetCommand({\n TableName: env.FLOW_CONFIGS_TABLE_NAME,\n Key: { id: flowConfigId },\n });\n\n const response = await docClient.send(getCommand);\n const existingConfig = response.Item;\n\n // Determine required permission level\n const action = existingConfig ? 'Edit' : 'Create';\n\n // Check permissions\n const accessLevel = await checkPermissions(claims, flowConfigId, action);\n if (!accessLevel) {\n return respondMessage(403, 'Access denied');\n }\n\n // Save to DynamoDB\n const putCommand = new PutCommand({\n TableName: env.FLOW_CONFIGS_TABLE_NAME,\n Item: body,\n });\n\n await docClient.send(putCommand);\n\n const statusCode = existingConfig ? 200 : 201;\n return respondObject(statusCode, body);\n } catch (error) {\n throw new Error(`Error saving flow config ${flowConfigId}: ${error}`);\n }\n}\n\nasync function deleteFlowConfig(\n flowConfigId: string,\n claims: Record<string, string>\n): Promise<APIGatewayProxyStructuredResultV2> {\n try {\n // Check permissions\n const accessLevel = await checkPermissions(claims, flowConfigId, 'Delete');\n if (!accessLevel) {\n return respondMessage(403, 'Access denied');\n }\n\n // Check if flow config exists\n const getCommand = new GetCommand({\n TableName: env.FLOW_CONFIGS_TABLE_NAME,\n Key: { id: flowConfigId },\n });\n\n const response = await docClient.send(getCommand);\n\n if (!response.Item) {\n return respondMessage(404, 'Flow Config not found');\n }\n\n // Delete from DynamoDB\n const deleteCommand = new DeleteCommand({\n TableName: env.FLOW_CONFIGS_TABLE_NAME,\n Key: { id: flowConfigId },\n });\n\n await docClient.send(deleteCommand);\n\n return respondMessage(204, '');\n } catch (error) {\n throw new Error(`Error deleting flow config ${flowConfigId}: ${error}`);\n }\n}\n\nasync function previewFlowConfig(\n event: APIGatewayProxyEvent,\n claims: Record<string, string>\n): Promise<APIGatewayProxyStructuredResultV2> {\n try {\n // Parse request body\n if (!event.body) {\n return respondMessage(400, 'Request body required');\n }\n\n let requestData;\n try {\n requestData = JSON.parse(event.body);\n } catch (error) {\n return respondMessage(400, 'Invalid JSON in request body');\n }\n\n // Extract and validate required fields\n const { flowConfig, lang: language, channel } = requestData;\n\n if (!flowConfig) {\n return respondMessage(400, 'flowConfig is required');\n }\n\n if (!language) {\n return respondMessage(400, 'lang is required');\n }\n\n if (!channel) {\n return respondMessage(400, 'channel is required');\n }\n\n // Validate parameters\n if (!/^[a-z]{2}-[A-Z]{2}$/.test(language)) {\n return respondMessage(\n 400,\n 'Invalid language code format. Expected format: en-US'\n );\n }\n\n if (!['voice', 'chat'].includes(channel)) {\n return respondMessage(400, 'Invalid channel. Must be \"voice\" or \"chat\"');\n }\n\n // Basic validation of flowConfig structure\n if (\n !flowConfig.id ||\n !flowConfig.description ||\n !flowConfig.variables ||\n !flowConfig.prompts\n ) {\n return respondMessage(\n 400,\n 'FlowConfig must have id, description, variables, and prompts'\n );\n }\n\n // Check permissions for the flow config ID\n const accessLevel = await checkPermissions(claims, flowConfig.id, 'Read');\n if (!accessLevel) {\n return respondMessage(403, 'Access denied');\n }\n\n // Use shared transformation function directly\n const result = transformFlowConfig(flowConfig, {\n language,\n channel: channel as 'voice' | 'chat',\n });\n\n return respondObject(200, result);\n } catch (error) {\n throw new Error(`Error previewing flow config: ${error}`);\n }\n}\n\nasync function checkPermissions(\n _claims: Record<string, string>,\n flowConfigId: string,\n action: string\n): Promise<'Full' | 'Edit' | 'Read' | null> {\n try {\n return Promise.resolve('Full');\n } catch (error) {\n console.error(\n `Error checking permissions for ${flowConfigId}, action ${action}:`,\n error\n );\n return null;\n }\n}\n", "import { Logger } from '@aws-lambda-powertools/logger';\nimport { ConnectContactFlowEvent, Context } from 'aws-lambda';\n\nexport const logger = new Logger();\n\n/**\n * Call this method in your lambda handler to capture event details\n * @todo use logger.appendKeys to add attributes like ContactId\n */\nexport const logEvent = (event?: unknown, context?: Context) => {\n if (context) {\n logger.addContext(context);\n }\n if (event) {\n logger.logEventIfEnabled(event);\n }\n\n const ContactData = (event as ConnectContactFlowEvent)?.Details?.ContactData;\n if (ContactData) {\n const { InstanceARN, ContactId } = ContactData;\n logger.appendKeys({\n connectInstanceId: InstanceARN?.split('/').pop(),\n connectContactId: ContactId\n });\n }\n};\n", "// Utility functions for consistent API response\n\nimport { logger } from './logger';\n\nexport const respond = (statusCode: number, body: string) => ({\n statusCode,\n body,\n});\n\nexport const respondObject = (statusCode: number, obj?: object) =>\n respond(statusCode, JSON.stringify(obj));\n\nexport const respondMessage = (statusCode: number, message: string) =>\n respondObject(statusCode, { message });\n\n/**\n * Log error and respond with HTTP 500\n * Note: You can conditionally hide the message from the user based on environment\n */\nexport const respondError = (error: unknown) => {\n logger.error('Unhandled Server Error', error as Error);\n return respondMessage(500, (error as Error).message);\n};\n", "import { PublishCommand, SNSClient } from '@aws-sdk/client-sns';\nimport { logger } from './logger';\nimport { getVar } from './getVar';\n\nconst client = new SNSClient();\n\nconst ALERT_TOPIC_ARN = getVar('ALERT_TOPIC_ARN');\n\n/**\n * Send unhandled exceptions to admins so that the issue can be handled before a client complains\n */\nexport const sendError = async (\n subject: string,\n error: string | Error\n): Promise<void> => {\n try {\n await client.send(\n new PublishCommand({\n TopicArn: ALERT_TOPIC_ARN,\n Subject: subject,\n Message: typeof error === 'string' ? error : error.message,\n })\n );\n } catch (error) {\n logger.error('Error sending message to SNS', {\n error,\n sns: { subject, message: error },\n });\n }\n};\n", "/**\n * Get environment variable and throw a descriptive error if its undefined\n */\nexport function getVar(name: string, defaultValue?: string): string {\n const val = process.env[name] || defaultValue;\n if (!val) {\n throw new Error(`Environment variable \"${name}\" is not defined`);\n }\n return val;\n}\n", "import { logger } from './logger';\nimport { FlowConfig } from './models';\n\nexport interface TransformOptions {\n language: string;\n channel: 'voice' | 'chat';\n}\n\n/**\n * Transform a FlowConfig object into a Record<string, string> for Amazon Connect\n * This function is shared between the preview API and GetConfig lambda\n */\nexport function transformFlowConfig(\n config: FlowConfig,\n options: TransformOptions\n): Record<string, string> {\n const { language, channel } = options;\n\n logger.debug('Transforming flow config', {\n configId: config.id,\n language,\n channel,\n });\n\n // Extract variables\n const variables = config.variables || {};\n\n // Resolve prompts for the specified language and channel\n const prompts: Record<string, string> = {};\n const rawPrompts = config.prompts || {};\n\n for (const [promptName, promptData] of Object.entries(rawPrompts)) {\n if (language in promptData) {\n const langData = promptData[language];\n\n // Use channel-specific prompt, fallback to voice\n if (channel === 'chat' && langData.chat) {\n prompts[promptName] = langData.chat;\n } else if (langData.voice) {\n // For chat channel without chat content, strip SSML tags from voice content\n if (channel === 'chat') {\n prompts[promptName] = stripSSML(langData.voice);\n } else {\n prompts[promptName] = langData.voice;\n }\n }\n } else {\n logger.warn(`Language ${language} not found for prompt ${promptName}`, {\n configId: config.id,\n promptName,\n availableLanguages: Object.keys(promptData),\n });\n }\n }\n\n const result: Record<string, string> = {\n ...variables,\n ...prompts,\n };\n\n // Check response size (Amazon Connect has 32KB limit)\n const responseSize = JSON.stringify(result).length;\n if (responseSize > 30000) {\n // Leave some buffer\n logger.warn('Response size approaching Amazon Connect limit', {\n responseSize,\n configId: config.id,\n limit: 32768,\n });\n }\n\n logger.info('Successfully transformed FlowConfig', {\n configId: config.id,\n language,\n channel,\n variableCount: Object.keys(variables).length,\n promptCount: Object.keys(prompts).length,\n responseSize,\n });\n\n return result;\n}\n\n/**\n * Strip SSML tags from voice content for chat channel\n */\nfunction stripSSML(text: string): string {\n // Remove SSML tags but keep the content\n return text\n .replace(/<[^>]*>/g, '') // Remove all XML/SSML tags\n .replace(/\\s+/g, ' ') // Normalize whitespace\n .trim();\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
3
|
+
"sources": ["../../../backend/FlowConfig.ts", "../../../backend/shared/logger.ts", "../../../backend/shared/respond.ts", "../../../backend/shared/snsClient.ts", "../../../backend/shared/getVar.ts", "../../../backend/shared/transformFlowConfig.ts", "../../../backend/shared/permissions.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n APIGatewayProxyEvent,\n APIGatewayProxyStructuredResultV2,\n Context,\n} from 'aws-lambda';\nimport { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport {\n DynamoDBDocumentClient,\n GetCommand,\n ScanCommand,\n PutCommand,\n DeleteCommand,\n} from '@aws-sdk/lib-dynamodb';\nimport { logEvent } from './shared/logger';\nimport { FlowConfigEnv } from '../infrastructure/api/FlowConfig/FlowConfig.interface';\nimport { respondError, respondMessage, respondObject } from './shared/respond';\nimport { sendError } from './shared/snsClient';\nimport { transformFlowConfig } from './shared/transformFlowConfig';\nimport { validateFlowConfigPermission } from './shared/permissions';\n\nconst env = process.env as unknown as FlowConfigEnv;\nconst client = new DynamoDBClient();\nconst docClient = DynamoDBDocumentClient.from(client);\nimport { FlowConfig, FlowConfigList, FlowConfigSummary } from './shared/models';\nexport const handler = async (\n event: APIGatewayProxyEvent,\n context?: Context\n): Promise<APIGatewayProxyStructuredResultV2> => {\n logEvent(event, context);\n\n try {\n const method = event.httpMethod;\n const path = event.path;\n const pathParameters = event.pathParameters;\n\n // Extract user claims from Cognito authorizer\n const claims = event.requestContext.authorizer?.claims;\n\n if (!claims) {\n return respondObject(401, new Error('Unauthorized'));\n }\n\n // Route to appropriate handler\n if (method === 'GET' && path === '/api/flow-config') {\n return await listFlowConfigs(event, claims);\n } else if (method === 'GET' && pathParameters?.id) {\n return await getFlowConfig(pathParameters.id, claims);\n } else if (method === 'POST' && path === '/api/flow-config/preview') {\n return await previewFlowConfig(event, claims);\n } else if (method === 'POST' && pathParameters?.id) {\n return await saveFlowConfig(pathParameters.id, event, claims);\n } else if (method === 'DELETE' && pathParameters?.id) {\n return await deleteFlowConfig(pathParameters.id, claims);\n }\n\n return respondMessage(404, 'Not Found');\n } catch (error) {\n await sendError('Unhandled Error: api/flow-config', error as Error);\n return respondError(error);\n }\n};\n\nasync function listFlowConfigs(\n event: APIGatewayProxyEvent,\n claims: Record<string, string>\n): Promise<APIGatewayProxyStructuredResultV2> {\n try {\n // Get all flow configs from DynamoDB\n const scanCommand = new ScanCommand({\n TableName: env.FLOW_CONFIGS_TABLE_NAME,\n });\n\n const response = await docClient.send(scanCommand);\n const flowConfigs = response.Items || [];\n\n // Get query parameters for filtering\n const pattern = event.queryStringParameters?.pattern;\n\n // Filter by pattern if provided\n let filteredConfigs = flowConfigs;\n if (pattern) {\n filteredConfigs = flowConfigs.filter((config) =>\n config.id.startsWith(pattern)\n );\n }\n\n // Check permissions for each flow config\n const resultItems: FlowConfigSummary[] = [];\n\n for (const config of filteredConfigs) {\n // Check access level using user claims\n const accessLevel = validateFlowConfigPermission(claims, config.id, 'Read');\n if (accessLevel) {\n resultItems.push({\n id: config.id,\n description: config.description,\n accessLevel,\n });\n }\n }\n\n const result: FlowConfigList = { items: resultItems };\n return respondObject(200, result);\n } catch (error) {\n throw new Error(`Error listing flow configs: ${error}`);\n }\n}\n\nasync function getFlowConfig(\n flowConfigId: string,\n claims: Record<string, string>\n): Promise<APIGatewayProxyStructuredResultV2> {\n try {\n // Check permissions\n const accessLevel = validateFlowConfigPermission(claims, flowConfigId, 'Read');\n if (!accessLevel) {\n return respondMessage(403, 'Access denied');\n }\n\n // Get flow config from DynamoDB\n const getCommand = new GetCommand({\n TableName: env.FLOW_CONFIGS_TABLE_NAME,\n Key: { id: flowConfigId },\n });\n\n const response = await docClient.send(getCommand);\n\n if (!response.Item) {\n return respondMessage(404, 'Flow Config not found');\n }\n\n return respondObject(200, response.Item as FlowConfig);\n } catch (error) {\n throw new Error(`Error getting flow config ${flowConfigId}: ${error}`);\n }\n}\n\nasync function saveFlowConfig(\n flowConfigId: string,\n event: APIGatewayProxyEvent,\n claims: Record<string, string>\n): Promise<APIGatewayProxyStructuredResultV2> {\n // Parse and validate request body first (outside try-catch)\n if (!event.body) {\n return respondMessage(400, 'Request body required');\n }\n\n let body: FlowConfig;\n try {\n body = JSON.parse(event.body) as FlowConfig;\n } catch (error) {\n return respondMessage(400, 'Invalid JSON in request body');\n }\n\n // Ensure ID in body matches path parameter\n body.id = flowConfigId;\n\n // Basic validation\n if (!body.description || !body.variables || !body.prompts) {\n return respondMessage(\n 400,\n 'Missing required fields: description, variables, prompts'\n );\n }\n\n // Validate variables are strings\n for (const [key, value] of Object.entries(body.variables)) {\n if (typeof value !== 'string') {\n return respondMessage(400, `Variable ${key} must be a string`);\n }\n }\n\n // Validate prompts structure\n for (const [promptName, promptData] of Object.entries(body.prompts)) {\n for (const [lang, langData] of Object.entries(promptData)) {\n if (!langData.voice) {\n return respondMessage(\n 400,\n `Prompt ${promptName} for language ${lang} must have a voice variant`\n );\n }\n }\n }\n\n // Check size constraints (approximate)\n const itemSize = JSON.stringify(body).length;\n if (itemSize > 380000) {\n // Leave some buffer for DynamoDB 400KB limit\n return respondMessage(413, 'Flow config exceeds maximum size limit');\n }\n\n try {\n // Check if flow config exists\n const getCommand = new GetCommand({\n TableName: env.FLOW_CONFIGS_TABLE_NAME,\n Key: { id: flowConfigId },\n });\n\n const response = await docClient.send(getCommand);\n const existingConfig = response.Item;\n\n // Determine required permission level\n const action = existingConfig ? 'Edit' : 'Create';\n\n // Check permissions\n const accessLevel = validateFlowConfigPermission(claims, flowConfigId, action);\n if (!accessLevel) {\n return respondMessage(403, 'Access denied');\n }\n\n // For FlowConfigEdit users, validate they're only changing values, not structure\n if (accessLevel === 'Edit' && existingConfig) {\n const structuralChangeError = validateEditOnlyChanges(existingConfig as FlowConfig, body);\n if (structuralChangeError) {\n return respondMessage(403, `FlowConfigEdit users cannot make structural changes: ${structuralChangeError}`);\n }\n }\n\n // Save to DynamoDB\n const putCommand = new PutCommand({\n TableName: env.FLOW_CONFIGS_TABLE_NAME,\n Item: body,\n });\n\n await docClient.send(putCommand);\n\n const statusCode = existingConfig ? 200 : 201;\n return respondObject(statusCode, body);\n } catch (error) {\n throw new Error(`Error saving flow config ${flowConfigId}: ${error}`);\n }\n}\n\nasync function deleteFlowConfig(\n flowConfigId: string,\n claims: Record<string, string>\n): Promise<APIGatewayProxyStructuredResultV2> {\n try {\n // Check permissions\n const accessLevel = validateFlowConfigPermission(claims, flowConfigId, 'Delete');\n if (!accessLevel) {\n return respondMessage(403, 'Access denied');\n }\n\n // Check if flow config exists\n const getCommand = new GetCommand({\n TableName: env.FLOW_CONFIGS_TABLE_NAME,\n Key: { id: flowConfigId },\n });\n\n const response = await docClient.send(getCommand);\n\n if (!response.Item) {\n return respondMessage(404, 'Flow Config not found');\n }\n\n // Delete from DynamoDB\n const deleteCommand = new DeleteCommand({\n TableName: env.FLOW_CONFIGS_TABLE_NAME,\n Key: { id: flowConfigId },\n });\n\n await docClient.send(deleteCommand);\n\n return respondMessage(204, '');\n } catch (error) {\n throw new Error(`Error deleting flow config ${flowConfigId}: ${error}`);\n }\n}\n\nasync function previewFlowConfig(\n event: APIGatewayProxyEvent,\n claims: Record<string, string>\n): Promise<APIGatewayProxyStructuredResultV2> {\n try {\n // Parse request body\n if (!event.body) {\n return respondMessage(400, 'Request body required');\n }\n\n let requestData;\n try {\n requestData = JSON.parse(event.body);\n } catch (error) {\n return respondMessage(400, 'Invalid JSON in request body');\n }\n\n // Extract and validate required fields\n const { flowConfig, lang: language, channel } = requestData;\n\n if (!flowConfig) {\n return respondMessage(400, 'flowConfig is required');\n }\n\n if (!language) {\n return respondMessage(400, 'lang is required');\n }\n\n if (!channel) {\n return respondMessage(400, 'channel is required');\n }\n\n // Validate parameters\n if (!/^[a-z]{2}-[A-Z]{2}$/.test(language)) {\n return respondMessage(\n 400,\n 'Invalid language code format. Expected format: en-US'\n );\n }\n\n if (!['voice', 'chat'].includes(channel)) {\n return respondMessage(400, 'Invalid channel. Must be \"voice\" or \"chat\"');\n }\n\n // Basic validation of flowConfig structure\n if (\n !flowConfig.id ||\n !flowConfig.description ||\n !flowConfig.variables ||\n !flowConfig.prompts\n ) {\n return respondMessage(\n 400,\n 'FlowConfig must have id, description, variables, and prompts'\n );\n }\n\n // Check permissions for the flow config ID\n const accessLevel = validateFlowConfigPermission(claims, flowConfig.id, 'Read');\n if (!accessLevel) {\n return respondMessage(403, 'Access denied');\n }\n\n // Use shared transformation function directly\n const result = transformFlowConfig(flowConfig, {\n language,\n channel: channel as 'voice' | 'chat',\n });\n\n return respondObject(200, result);\n } catch (error) {\n throw new Error(`Error previewing flow config: ${error}`);\n }\n}\n\n/**\n * Validate that FlowConfigEdit users are only changing values, not structure\n * @param existingConfig The existing flow config from database\n * @param newConfig The new flow config being saved\n * @returns Error message if structural changes detected, null if only value changes\n */\nfunction validateEditOnlyChanges(existingConfig: FlowConfig, newConfig: FlowConfig): string | null {\n // Check if description changed (not allowed for Edit users)\n if (existingConfig.description !== newConfig.description) {\n return 'Cannot modify description';\n }\n\n // Check if variable keys changed (not allowed for Edit users)\n const existingVarKeys = Object.keys(existingConfig.variables || {}).sort();\n const newVarKeys = Object.keys(newConfig.variables || {}).sort();\n \n if (existingVarKeys.length !== newVarKeys.length || \n !existingVarKeys.every((key, index) => key === newVarKeys[index])) {\n return 'Cannot add or remove variables';\n }\n\n // Check if prompt structure changed (not allowed for Edit users)\n const existingPromptKeys = Object.keys(existingConfig.prompts || {}).sort();\n const newPromptKeys = Object.keys(newConfig.prompts || {}).sort();\n \n if (existingPromptKeys.length !== newPromptKeys.length || \n !existingPromptKeys.every((key, index) => key === newPromptKeys[index])) {\n return 'Cannot add or remove prompts';\n }\n\n // Check if languages were removed for each prompt (adding is allowed)\n for (const promptName of existingPromptKeys) {\n const existingPrompt = existingConfig.prompts[promptName];\n const newPrompt = newConfig.prompts[promptName];\n \n const existingLangs = Object.keys(existingPrompt || {});\n const newLangs = Object.keys(newPrompt || {});\n \n // Check if any existing languages were removed (not allowed)\n for (const existingLang of existingLangs) {\n if (!newLangs.includes(existingLang)) {\n return `Cannot remove language ${existingLang} from prompt ${promptName}`;\n }\n }\n \n // Adding languages and channels is allowed, so no further validation needed\n }\n\n return null; // No structural changes detected\n}\n\n", "import { Logger } from '@aws-lambda-powertools/logger';\nimport { ConnectContactFlowEvent, Context } from 'aws-lambda';\n\nexport const logger = new Logger();\n\n/**\n * Call this method in your lambda handler to capture event details\n * @todo use logger.appendKeys to add attributes like ContactId\n */\nexport const logEvent = (event?: unknown, context?: Context) => {\n if (context) {\n logger.addContext(context);\n }\n if (event) {\n logger.logEventIfEnabled(event);\n }\n\n const ContactData = (event as ConnectContactFlowEvent)?.Details?.ContactData;\n if (ContactData) {\n const { InstanceARN, ContactId } = ContactData;\n logger.appendKeys({\n connectInstanceId: InstanceARN?.split('/').pop(),\n connectContactId: ContactId\n });\n }\n};\n", "// Utility functions for consistent API response\n\nimport { logger } from './logger';\n\nexport const respond = (statusCode: number, body: string) => ({\n statusCode,\n body,\n});\n\nexport const respondObject = (statusCode: number, obj?: object) =>\n respond(statusCode, JSON.stringify(obj));\n\nexport const respondMessage = (statusCode: number, message: string) =>\n respondObject(statusCode, { message });\n\n/**\n * Log error and respond with HTTP 500\n * Note: You can conditionally hide the message from the user based on environment\n */\nexport const respondError = (error: unknown) => {\n logger.error('Unhandled Server Error', error as Error);\n return respondMessage(500, (error as Error).message);\n};\n", "import { PublishCommand, SNSClient } from '@aws-sdk/client-sns';\nimport { logger } from './logger';\nimport { getVar } from './getVar';\n\nconst client = new SNSClient();\n\nconst ALERT_TOPIC_ARN = getVar('ALERT_TOPIC_ARN');\n\n/**\n * Send unhandled exceptions to admins so that the issue can be handled before a client complains\n */\nexport const sendError = async (\n subject: string,\n error: string | Error\n): Promise<void> => {\n try {\n await client.send(\n new PublishCommand({\n TopicArn: ALERT_TOPIC_ARN,\n Subject: subject,\n Message: typeof error === 'string' ? error : error.message,\n })\n );\n } catch (error) {\n logger.error('Error sending message to SNS', {\n error,\n sns: { subject, message: error },\n });\n }\n};\n", "/**\n * Get environment variable and throw a descriptive error if its undefined\n */\nexport function getVar(name: string, defaultValue?: string): string {\n const val = process.env[name] || defaultValue;\n if (!val) {\n throw new Error(`Environment variable \"${name}\" is not defined`);\n }\n return val;\n}\n", "import { logger } from './logger';\nimport { FlowConfig } from './models';\n\nexport interface TransformOptions {\n language: string;\n channel: 'voice' | 'chat';\n}\n\n/**\n * Transform a FlowConfig object into a Record<string, string> for Amazon Connect\n * This function is shared between the preview API and GetConfig lambda\n */\nexport function transformFlowConfig(\n config: FlowConfig,\n options: TransformOptions\n): Record<string, string> {\n const { language, channel } = options;\n\n logger.debug('Transforming flow config', {\n configId: config.id,\n language,\n channel,\n });\n\n // Extract variables\n const variables = config.variables || {};\n\n // Resolve prompts for the specified language and channel\n const prompts: Record<string, string> = {};\n const rawPrompts = config.prompts || {};\n\n for (const [promptName, promptData] of Object.entries(rawPrompts)) {\n if (language in promptData) {\n const langData = promptData[language];\n\n // Use channel-specific prompt, fallback to voice\n if (channel === 'chat' && langData.chat) {\n prompts[promptName] = langData.chat;\n } else if (langData.voice) {\n // For chat channel without chat content, strip SSML tags from voice content\n if (channel === 'chat') {\n prompts[promptName] = stripSSML(langData.voice);\n } else {\n prompts[promptName] = langData.voice;\n }\n }\n } else {\n logger.warn(`Language ${language} not found for prompt ${promptName}`, {\n configId: config.id,\n promptName,\n availableLanguages: Object.keys(promptData),\n });\n }\n }\n\n const result: Record<string, string> = {\n ...variables,\n ...prompts,\n };\n\n // Check response size (Amazon Connect has 32KB limit)\n const responseSize = JSON.stringify(result).length;\n if (responseSize > 30000) {\n // Leave some buffer\n logger.warn('Response size approaching Amazon Connect limit', {\n responseSize,\n configId: config.id,\n limit: 32768,\n });\n }\n\n logger.info('Successfully transformed FlowConfig', {\n configId: config.id,\n language,\n channel,\n variableCount: Object.keys(variables).length,\n promptCount: Object.keys(prompts).length,\n responseSize,\n });\n\n return result;\n}\n\n/**\n * Strip SSML tags from voice content for chat channel\n */\nfunction stripSSML(text: string): string {\n // Remove SSML tags but keep the content\n return text\n .replace(/<[^>]*>/g, '') // Remove all XML/SSML tags\n .replace(/\\s+/g, ' ') // Normalize whitespace\n .trim();\n}\n", "/**\n * Cognito User Groups permission validation utilities\n * \n * This module provides functions to validate user permissions based on\n * Cognito User Group membership for role-based access control (RBAC).\n */\n\nexport type AccessLevel = 'Full' | 'Edit' | 'Read';\nexport type Action = 'Create' | 'Read' | 'Edit' | 'Delete';\n\n/**\n * Cognito User Groups for FlowConfig application\n */\nexport const COGNITO_GROUPS = {\n ADMIN: 'FlowConfigAdmin',\n EDIT: 'FlowConfigEdit',\n READ: 'FlowConfigRead',\n} as const;\n\n/**\n * Extract Cognito groups from user claims\n * @param claims User claims from Cognito JWT token\n * @returns Array of group names the user belongs to\n */\nexport function extractCognitoGroups(claims: Record<string, string>): string[] {\n // Cognito includes groups in the 'cognito:groups' claim as a comma-separated string\n const groupsClaim = claims['cognito:groups'];\n if (!groupsClaim) {\n return [];\n }\n \n // Handle both string and array formats\n if (typeof groupsClaim === 'string') {\n return groupsClaim.split(',').map(group => group.trim());\n }\n \n // If it's already an array (in some cases), return it\n if (Array.isArray(groupsClaim)) {\n return groupsClaim;\n }\n \n return [];\n}\n\n/**\n * Check if user has any FlowConfig group membership\n * @param claims User claims from Cognito JWT token\n * @returns true if user belongs to at least one FlowConfig group\n */\nexport function hasFlowConfigAccess(claims: Record<string, string>): boolean {\n const userGroups = extractCognitoGroups(claims);\n const flowConfigGroups = Object.values(COGNITO_GROUPS);\n \n return userGroups.some(group => flowConfigGroups.includes(group as any));\n}\n\n/**\n * Get the highest access level for a user based on their group memberships\n * @param claims User claims from Cognito JWT token\n * @returns Highest access level or null if no access\n */\nexport function getAccessLevel(claims: Record<string, string>): AccessLevel | null {\n const userGroups = extractCognitoGroups(claims);\n \n // Check in order of highest to lowest priority\n if (userGroups.includes(COGNITO_GROUPS.ADMIN)) {\n return 'Full';\n }\n \n if (userGroups.includes(COGNITO_GROUPS.EDIT)) {\n return 'Edit';\n }\n \n if (userGroups.includes(COGNITO_GROUPS.READ)) {\n return 'Read';\n }\n \n return null;\n}\n\n/**\n * Check if user has permission to perform a specific action\n * @param claims User claims from Cognito JWT token\n * @param action The action being performed\n * @returns AccessLevel if authorized, null if not authorized\n */\nexport function checkActionPermission(\n claims: Record<string, string>,\n action: Action\n): AccessLevel | null {\n const accessLevel = getAccessLevel(claims);\n \n if (!accessLevel) {\n return null;\n }\n \n // Map actions to required access levels\n switch (action) {\n case 'Read':\n // All groups can read\n return accessLevel;\n \n case 'Edit':\n // Edit and Admin can edit values\n if (accessLevel === 'Edit' || accessLevel === 'Full') {\n return accessLevel;\n }\n return null;\n \n case 'Create':\n case 'Delete':\n // Only Admin can create or delete\n if (accessLevel === 'Full') {\n return accessLevel;\n }\n return null;\n \n default:\n return null;\n }\n}\n\n/**\n * Check if user can perform a structural change (add/remove fields)\n * Only FlowConfigAdmin users can perform structural changes\n * @param claims User claims from Cognito JWT token\n * @returns true if user can make structural changes\n */\nexport function canMakeStructuralChanges(claims: Record<string, string>): boolean {\n const accessLevel = getAccessLevel(claims);\n return accessLevel === 'Full';\n}\n\n/**\n * Validate that a user has permission for a flow config operation\n * This is the main function to be used by API endpoints\n * @param claims User claims from Cognito JWT token\n * @param _flowConfigId The flow config ID (not used in v1, but kept for v2 compatibility)\n * @param action The action being performed\n * @returns AccessLevel if authorized, null if not authorized\n */\nexport function validateFlowConfigPermission(\n claims: Record<string, string>,\n _flowConfigId: string,\n action: Action\n): AccessLevel | null {\n // In v1, all permissions are global (flowConfigId is ignored)\n // This parameter is kept for v2 compatibility when per-config permissions are added\n \n if (!hasFlowConfigAccess(claims)) {\n return null;\n }\n \n return checkActionPermission(claims, action);\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["error", "client"]
|
|
7
7
|
}
|