@cxbuilder/flow-config 1.0.2 → 2.0.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.
Files changed (35) hide show
  1. package/.jsii +145 -68
  2. package/CHANGELOG.md +38 -0
  3. package/README.md +8 -5
  4. package/dist/backend/FlowConfig/index.js +111 -16
  5. package/dist/backend/FlowConfig/index.js.map +3 -3
  6. package/dist/backend/GetConfig/index.js +1 -1
  7. package/dist/backend/GetConfig/index.js.map +2 -2
  8. package/dist/backend/Init/index.js +2 -1
  9. package/dist/backend/Init/index.js.map +2 -2
  10. package/dist/backend/Settings/index.js +255 -0
  11. package/dist/backend/Settings/index.js.map +7 -0
  12. package/dist/backend/Static/static/assets/index-Cejunttu.js +61 -0
  13. package/dist/backend/Static/static/assets/{index-NRh8x3FI.css → index-SZuscj14.css} +1 -1
  14. package/dist/backend/Static/static/index.html +3 -3
  15. package/dist/infrastructure/FlowConfigStack.d.ts +36 -14
  16. package/dist/infrastructure/FlowConfigStack.js +54 -18
  17. package/dist/infrastructure/GetConfig/index.js +2 -2
  18. package/dist/infrastructure/api/Api.d.ts +5 -2
  19. package/dist/infrastructure/api/Api.js +21 -15
  20. package/dist/infrastructure/api/Init/Init.interface.d.ts +4 -0
  21. package/dist/infrastructure/api/Init/Init.interface.js +1 -1
  22. package/dist/infrastructure/api/Init/index.js +2 -1
  23. package/dist/infrastructure/api/Settings/Settings.interface.d.ts +3 -0
  24. package/dist/infrastructure/api/Settings/Settings.interface.js +3 -0
  25. package/dist/infrastructure/api/Settings/index.d.ts +7 -0
  26. package/dist/infrastructure/api/Settings/index.js +21 -0
  27. package/dist/infrastructure/api/spec.yaml +122 -0
  28. package/dist/infrastructure/createLambda.js +1 -1
  29. package/dist/infrastructure/index.d.ts +1 -1
  30. package/dist/infrastructure/index.js +1 -1
  31. package/dist/infrastructure/tsconfig.tsbuildinfo +1 -1
  32. package/docs/Permissions-v1.md +132 -0
  33. package/docs/{Permissions.md → Permissions-v2.md} +15 -15
  34. package/package.json +1 -1
  35. package/dist/backend/Static/static/assets/index-Bx9Z3cF9.js +0 -61
@@ -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();
@@ -188,7 +260,8 @@ async function listFlowConfigs(event, claims) {
188
260
  TableName: env.FLOW_CONFIGS_TABLE_NAME
189
261
  });
190
262
  const response = await docClient.send(scanCommand);
191
- const flowConfigs = response.Items || [];
263
+ const allItems = response.Items || [];
264
+ const flowConfigs = allItems.filter((item) => item.id !== "application-settings");
192
265
  const pattern = event.queryStringParameters?.pattern;
193
266
  let filteredConfigs = flowConfigs;
194
267
  if (pattern) {
@@ -198,7 +271,7 @@ async function listFlowConfigs(event, claims) {
198
271
  }
199
272
  const resultItems = [];
200
273
  for (const config of filteredConfigs) {
201
- const accessLevel = await checkPermissions(claims, config.id, "Read");
274
+ const accessLevel = validateFlowConfigPermission(claims, config.id, "Read");
202
275
  if (accessLevel) {
203
276
  resultItems.push({
204
277
  id: config.id,
@@ -216,7 +289,7 @@ async function listFlowConfigs(event, claims) {
216
289
  __name(listFlowConfigs, "listFlowConfigs");
217
290
  async function getFlowConfig(flowConfigId, claims) {
218
291
  try {
219
- const accessLevel = await checkPermissions(claims, flowConfigId, "Read");
292
+ const accessLevel = validateFlowConfigPermission(claims, flowConfigId, "Read");
220
293
  if (!accessLevel) {
221
294
  return respondMessage(403, "Access denied");
222
295
  }
@@ -278,10 +351,16 @@ async function saveFlowConfig(flowConfigId, event, claims) {
278
351
  const response = await docClient.send(getCommand);
279
352
  const existingConfig = response.Item;
280
353
  const action = existingConfig ? "Edit" : "Create";
281
- const accessLevel = await checkPermissions(claims, flowConfigId, action);
354
+ const accessLevel = validateFlowConfigPermission(claims, flowConfigId, action);
282
355
  if (!accessLevel) {
283
356
  return respondMessage(403, "Access denied");
284
357
  }
358
+ if (accessLevel === "Edit" && existingConfig) {
359
+ const structuralChangeError = validateEditOnlyChanges(existingConfig, body);
360
+ if (structuralChangeError) {
361
+ return respondMessage(403, `FlowConfigEdit users cannot make structural changes: ${structuralChangeError}`);
362
+ }
363
+ }
285
364
  const putCommand = new import_lib_dynamodb.PutCommand({
286
365
  TableName: env.FLOW_CONFIGS_TABLE_NAME,
287
366
  Item: body
@@ -296,7 +375,7 @@ async function saveFlowConfig(flowConfigId, event, claims) {
296
375
  __name(saveFlowConfig, "saveFlowConfig");
297
376
  async function deleteFlowConfig(flowConfigId, claims) {
298
377
  try {
299
- const accessLevel = await checkPermissions(claims, flowConfigId, "Delete");
378
+ const accessLevel = validateFlowConfigPermission(claims, flowConfigId, "Delete");
300
379
  if (!accessLevel) {
301
380
  return respondMessage(403, "Access denied");
302
381
  }
@@ -355,7 +434,7 @@ async function previewFlowConfig(event, claims) {
355
434
  "FlowConfig must have id, description, variables, and prompts"
356
435
  );
357
436
  }
358
- const accessLevel = await checkPermissions(claims, flowConfig.id, "Read");
437
+ const accessLevel = validateFlowConfigPermission(claims, flowConfig.id, "Read");
359
438
  if (!accessLevel) {
360
439
  return respondMessage(403, "Access denied");
361
440
  }
@@ -369,18 +448,34 @@ async function previewFlowConfig(event, claims) {
369
448
  }
370
449
  }
371
450
  __name(previewFlowConfig, "previewFlowConfig");
372
- async function checkPermissions(_claims, flowConfigId, action) {
373
- try {
374
- return Promise.resolve("Full");
375
- } catch (error) {
376
- console.error(
377
- `Error checking permissions for ${flowConfigId}, action ${action}:`,
378
- error
379
- );
380
- return null;
451
+ function validateEditOnlyChanges(existingConfig, newConfig) {
452
+ if (existingConfig.description !== newConfig.description) {
453
+ return "Cannot modify description";
454
+ }
455
+ const existingVarKeys = Object.keys(existingConfig.variables || {}).sort();
456
+ const newVarKeys = Object.keys(newConfig.variables || {}).sort();
457
+ if (existingVarKeys.length !== newVarKeys.length || !existingVarKeys.every((key, index) => key === newVarKeys[index])) {
458
+ return "Cannot add or remove variables";
459
+ }
460
+ const existingPromptKeys = Object.keys(existingConfig.prompts || {}).sort();
461
+ const newPromptKeys = Object.keys(newConfig.prompts || {}).sort();
462
+ if (existingPromptKeys.length !== newPromptKeys.length || !existingPromptKeys.every((key, index) => key === newPromptKeys[index])) {
463
+ return "Cannot add or remove prompts";
464
+ }
465
+ for (const promptName of existingPromptKeys) {
466
+ const existingPrompt = existingConfig.prompts[promptName];
467
+ const newPrompt = newConfig.prompts[promptName];
468
+ const existingLangs = Object.keys(existingPrompt || {});
469
+ const newLangs = Object.keys(newPrompt || {});
470
+ for (const existingLang of existingLangs) {
471
+ if (!newLangs.includes(existingLang)) {
472
+ return `Cannot remove language ${existingLang} from prompt ${promptName}`;
473
+ }
474
+ }
381
475
  }
476
+ return null;
382
477
  }
383
- __name(checkPermissions, "checkPermissions");
478
+ __name(validateEditOnlyChanges, "validateEditOnlyChanges");
384
479
  // Annotate the CommonJS export names for ESM import in node:
385
480
  0 && (module.exports = {
386
481
  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": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,6BAA+B;AAC/B,0BAMO;;;ACZP,oBAAuB;AAGhB,IAAM,SAAS,IAAI,qBAAO;AAM1B,IAAM,WAAW,wBAAC,OAAiB,YAAsB;AAC9D,MAAI,SAAS;AACX,WAAO,WAAW,OAAO;AAAA,EAC3B;AACA,MAAI,OAAO;AACT,WAAO,kBAAkB,KAAK;AAAA,EAChC;AAEA,QAAM,cAAe,OAAmC,SAAS;AACjE,MAAI,aAAa;AACf,UAAM,EAAE,aAAa,UAAU,IAAI;AACnC,WAAO,WAAW;AAAA,MAChB,mBAAmB,aAAa,MAAM,GAAG,EAAE,IAAI;AAAA,MAC/C,kBAAkB;AAAA,IACpB,CAAC;AAAA,EACH;AACF,GAhBwB;;;ACLjB,IAAM,UAAU,wBAAC,YAAoB,UAAkB;AAAA,EAC5D;AAAA,EACA;AACF,IAHuB;AAKhB,IAAM,gBAAgB,wBAAC,YAAoB,QAChD,QAAQ,YAAY,KAAK,UAAU,GAAG,CAAC,GADZ;AAGtB,IAAM,iBAAiB,wBAAC,YAAoB,YACjD,cAAc,YAAY,EAAE,QAAQ,CAAC,GADT;AAOvB,IAAM,eAAe,wBAAC,UAAmB;AAC9C,SAAO,MAAM,0BAA0B,KAAc;AACrD,SAAO,eAAe,KAAM,MAAgB,OAAO;AACrD,GAH4B;;;ACnB5B,wBAA0C;;;ACGnC,SAAS,OAAO,MAAc,cAA+B;AAClE,QAAM,MAAM,QAAQ,IAAI,IAAI,KAAK;AACjC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,yBAAyB,IAAI,kBAAkB;AAAA,EACjE;AACA,SAAO;AACT;AANgB;;;ADChB,IAAM,SAAS,IAAI,4BAAU;AAE7B,IAAM,kBAAkB,OAAO,iBAAiB;AAKzC,IAAM,YAAY,8BACvB,SACA,UACkB;AAClB,MAAI;AACF,UAAM,OAAO;AAAA,MACX,IAAI,iCAAe;AAAA,QACjB,UAAU;AAAA,QACV,SAAS;AAAA,QACT,SAAS,OAAO,UAAU,WAAW,QAAQ,MAAM;AAAA,MACrD,CAAC;AAAA,IACH;AAAA,EACF,SAASA,QAAO;AACd,WAAO,MAAM,gCAAgC;AAAA,MAC3C,OAAAA;AAAA,MACA,KAAK,EAAE,SAAS,SAASA,OAAM;AAAA,IACjC,CAAC;AAAA,EACH;AACF,GAlByB;;;AEClB,SAAS,oBACd,QACA,SACwB;AACxB,QAAM,EAAE,UAAU,QAAQ,IAAI;AAE9B,SAAO,MAAM,4BAA4B;AAAA,IACvC,UAAU,OAAO;AAAA,IACjB;AAAA,IACA;AAAA,EACF,CAAC;AAGD,QAAM,YAAY,OAAO,aAAa,CAAC;AAGvC,QAAM,UAAkC,CAAC;AACzC,QAAM,aAAa,OAAO,WAAW,CAAC;AAEtC,aAAW,CAAC,YAAY,UAAU,KAAK,OAAO,QAAQ,UAAU,GAAG;AACjE,QAAI,YAAY,YAAY;AAC1B,YAAM,WAAW,WAAW,QAAQ;AAGpC,UAAI,YAAY,UAAU,SAAS,MAAM;AACvC,gBAAQ,UAAU,IAAI,SAAS;AAAA,MACjC,WAAW,SAAS,OAAO;AAEzB,YAAI,YAAY,QAAQ;AACtB,kBAAQ,UAAU,IAAI,UAAU,SAAS,KAAK;AAAA,QAChD,OAAO;AACL,kBAAQ,UAAU,IAAI,SAAS;AAAA,QACjC;AAAA,MACF;AAAA,IACF,OAAO;AACL,aAAO,KAAK,YAAY,QAAQ,yBAAyB,UAAU,IAAI;AAAA,QACrE,UAAU,OAAO;AAAA,QACjB;AAAA,QACA,oBAAoB,OAAO,KAAK,UAAU;AAAA,MAC5C,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,SAAiC;AAAA,IACrC,GAAG;AAAA,IACH,GAAG;AAAA,EACL;AAGA,QAAM,eAAe,KAAK,UAAU,MAAM,EAAE;AAC5C,MAAI,eAAe,KAAO;AAExB,WAAO,KAAK,kDAAkD;AAAA,MAC5D;AAAA,MACA,UAAU,OAAO;AAAA,MACjB,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,SAAO,KAAK,uCAAuC;AAAA,IACjD,UAAU,OAAO;AAAA,IACjB;AAAA,IACA;AAAA,IACA,eAAe,OAAO,KAAK,SAAS,EAAE;AAAA,IACtC,aAAa,OAAO,KAAK,OAAO,EAAE;AAAA,IAClC;AAAA,EACF,CAAC;AAED,SAAO;AACT;AArEgB;AA0EhB,SAAS,UAAU,MAAsB;AAEvC,SAAO,KACJ,QAAQ,YAAY,EAAE,EACtB,QAAQ,QAAQ,GAAG,EACnB,KAAK;AACV;AANS;;;ALnET,IAAM,MAAM,QAAQ;AACpB,IAAMC,UAAS,IAAI,sCAAe;AAClC,IAAM,YAAY,2CAAuB,KAAKA,OAAM;AAE7C,IAAM,UAAU,8BACrB,OACA,YAC+C;AAC/C,WAAS,OAAO,OAAO;AAEvB,MAAI;AACF,UAAM,SAAS,MAAM;AACrB,UAAM,OAAO,MAAM;AACnB,UAAM,iBAAiB,MAAM;AAG7B,UAAM,SAAS,MAAM,eAAe,YAAY;AAEhD,QAAI,CAAC,QAAQ;AACX,aAAO,cAAc,KAAK,IAAI,MAAM,cAAc,CAAC;AAAA,IACrD;AAGA,QAAI,WAAW,SAAS,SAAS,oBAAoB;AACnD,aAAO,MAAM,gBAAgB,OAAO,MAAM;AAAA,IAC5C,WAAW,WAAW,SAAS,gBAAgB,IAAI;AACjD,aAAO,MAAM,cAAc,eAAe,IAAI,MAAM;AAAA,IACtD,WAAW,WAAW,UAAU,SAAS,4BAA4B;AACnE,aAAO,MAAM,kBAAkB,OAAO,MAAM;AAAA,IAC9C,WAAW,WAAW,UAAU,gBAAgB,IAAI;AAClD,aAAO,MAAM,eAAe,eAAe,IAAI,OAAO,MAAM;AAAA,IAC9D,WAAW,WAAW,YAAY,gBAAgB,IAAI;AACpD,aAAO,MAAM,iBAAiB,eAAe,IAAI,MAAM;AAAA,IACzD;AAEA,WAAO,eAAe,KAAK,WAAW;AAAA,EACxC,SAAS,OAAO;AACd,UAAM,UAAU,oCAAoC,KAAc;AAClE,WAAO,aAAa,KAAK;AAAA,EAC3B;AACF,GApCuB;AAsCvB,eAAe,gBACb,OACA,QAC4C;AAC5C,MAAI;AAEF,UAAM,cAAc,IAAI,gCAAY;AAAA,MAClC,WAAW,IAAI;AAAA,IACjB,CAAC;AAED,UAAM,WAAW,MAAM,UAAU,KAAK,WAAW;AACjD,UAAM,cAAc,SAAS,SAAS,CAAC;AAGvC,UAAM,UAAU,MAAM,uBAAuB;AAG7C,QAAI,kBAAkB;AACtB,QAAI,SAAS;AACX,wBAAkB,YAAY;AAAA,QAAO,CAAC,WACpC,OAAO,GAAG,WAAW,OAAO;AAAA,MAC9B;AAAA,IACF;AAGA,UAAM,cAAmC,CAAC;AAE1C,eAAW,UAAU,iBAAiB;AAEpC,YAAM,cAAc,MAAM,iBAAiB,QAAQ,OAAO,IAAI,MAAM;AACpE,UAAI,aAAa;AACf,oBAAY,KAAK;AAAA,UACf,IAAI,OAAO;AAAA,UACX,aAAa,OAAO;AAAA,UACpB;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAEA,UAAM,SAAyB,EAAE,OAAO,YAAY;AACpD,WAAO,cAAc,KAAK,MAAM;AAAA,EAClC,SAAS,OAAO;AACd,UAAM,IAAI,MAAM,+BAA+B,KAAK,EAAE;AAAA,EACxD;AACF;AA5Ce;AA8Cf,eAAe,cACb,cACA,QAC4C;AAC5C,MAAI;AAEF,UAAM,cAAc,MAAM,iBAAiB,QAAQ,cAAc,MAAM;AACvE,QAAI,CAAC,aAAa;AAChB,aAAO,eAAe,KAAK,eAAe;AAAA,IAC5C;AAGA,UAAM,aAAa,IAAI,+BAAW;AAAA,MAChC,WAAW,IAAI;AAAA,MACf,KAAK,EAAE,IAAI,aAAa;AAAA,IAC1B,CAAC;AAED,UAAM,WAAW,MAAM,UAAU,KAAK,UAAU;AAEhD,QAAI,CAAC,SAAS,MAAM;AAClB,aAAO,eAAe,KAAK,uBAAuB;AAAA,IACpD;AAEA,WAAO,cAAc,KAAK,SAAS,IAAkB;AAAA,EACvD,SAAS,OAAO;AACd,UAAM,IAAI,MAAM,6BAA6B,YAAY,KAAK,KAAK,EAAE;AAAA,EACvE;AACF;AA3Be;AA6Bf,eAAe,eACb,cACA,OACA,QAC4C;AAE5C,MAAI,CAAC,MAAM,MAAM;AACf,WAAO,eAAe,KAAK,uBAAuB;AAAA,EACpD;AAEA,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,MAAM,MAAM,IAAI;AAAA,EAC9B,SAAS,OAAO;AACd,WAAO,eAAe,KAAK,8BAA8B;AAAA,EAC3D;AAGA,OAAK,KAAK;AAGV,MAAI,CAAC,KAAK,eAAe,CAAC,KAAK,aAAa,CAAC,KAAK,SAAS;AACzD,WAAO;AAAA,MACL;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAGA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,SAAS,GAAG;AACzD,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO,eAAe,KAAK,YAAY,GAAG,mBAAmB;AAAA,IAC/D;AAAA,EACF;AAGA,aAAW,CAAC,YAAY,UAAU,KAAK,OAAO,QAAQ,KAAK,OAAO,GAAG;AACnE,eAAW,CAAC,MAAM,QAAQ,KAAK,OAAO,QAAQ,UAAU,GAAG;AACzD,UAAI,CAAC,SAAS,OAAO;AACnB,eAAO;AAAA,UACL;AAAA,UACA,UAAU,UAAU,iBAAiB,IAAI;AAAA,QAC3C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,QAAM,WAAW,KAAK,UAAU,IAAI,EAAE;AACtC,MAAI,WAAW,MAAQ;AAErB,WAAO,eAAe,KAAK,wCAAwC;AAAA,EACrE;AAEA,MAAI;AAEF,UAAM,aAAa,IAAI,+BAAW;AAAA,MAChC,WAAW,IAAI;AAAA,MACf,KAAK,EAAE,IAAI,aAAa;AAAA,IAC1B,CAAC;AAED,UAAM,WAAW,MAAM,UAAU,KAAK,UAAU;AAChD,UAAM,iBAAiB,SAAS;AAGhC,UAAM,SAAS,iBAAiB,SAAS;AAGzC,UAAM,cAAc,MAAM,iBAAiB,QAAQ,cAAc,MAAM;AACvE,QAAI,CAAC,aAAa;AAChB,aAAO,eAAe,KAAK,eAAe;AAAA,IAC5C;AAGA,UAAM,aAAa,IAAI,+BAAW;AAAA,MAChC,WAAW,IAAI;AAAA,MACf,MAAM;AAAA,IACR,CAAC;AAED,UAAM,UAAU,KAAK,UAAU;AAE/B,UAAM,aAAa,iBAAiB,MAAM;AAC1C,WAAO,cAAc,YAAY,IAAI;AAAA,EACvC,SAAS,OAAO;AACd,UAAM,IAAI,MAAM,4BAA4B,YAAY,KAAK,KAAK,EAAE;AAAA,EACtE;AACF;AAtFe;AAwFf,eAAe,iBACb,cACA,QAC4C;AAC5C,MAAI;AAEF,UAAM,cAAc,MAAM,iBAAiB,QAAQ,cAAc,QAAQ;AACzE,QAAI,CAAC,aAAa;AAChB,aAAO,eAAe,KAAK,eAAe;AAAA,IAC5C;AAGA,UAAM,aAAa,IAAI,+BAAW;AAAA,MAChC,WAAW,IAAI;AAAA,MACf,KAAK,EAAE,IAAI,aAAa;AAAA,IAC1B,CAAC;AAED,UAAM,WAAW,MAAM,UAAU,KAAK,UAAU;AAEhD,QAAI,CAAC,SAAS,MAAM;AAClB,aAAO,eAAe,KAAK,uBAAuB;AAAA,IACpD;AAGA,UAAM,gBAAgB,IAAI,kCAAc;AAAA,MACtC,WAAW,IAAI;AAAA,MACf,KAAK,EAAE,IAAI,aAAa;AAAA,IAC1B,CAAC;AAED,UAAM,UAAU,KAAK,aAAa;AAElC,WAAO,eAAe,KAAK,EAAE;AAAA,EAC/B,SAAS,OAAO;AACd,UAAM,IAAI,MAAM,8BAA8B,YAAY,KAAK,KAAK,EAAE;AAAA,EACxE;AACF;AAnCe;AAqCf,eAAe,kBACb,OACA,QAC4C;AAC5C,MAAI;AAEF,QAAI,CAAC,MAAM,MAAM;AACf,aAAO,eAAe,KAAK,uBAAuB;AAAA,IACpD;AAEA,QAAI;AACJ,QAAI;AACF,oBAAc,KAAK,MAAM,MAAM,IAAI;AAAA,IACrC,SAAS,OAAO;AACd,aAAO,eAAe,KAAK,8BAA8B;AAAA,IAC3D;AAGA,UAAM,EAAE,YAAY,MAAM,UAAU,QAAQ,IAAI;AAEhD,QAAI,CAAC,YAAY;AACf,aAAO,eAAe,KAAK,wBAAwB;AAAA,IACrD;AAEA,QAAI,CAAC,UAAU;AACb,aAAO,eAAe,KAAK,kBAAkB;AAAA,IAC/C;AAEA,QAAI,CAAC,SAAS;AACZ,aAAO,eAAe,KAAK,qBAAqB;AAAA,IAClD;AAGA,QAAI,CAAC,sBAAsB,KAAK,QAAQ,GAAG;AACzC,aAAO;AAAA,QACL;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,QAAI,CAAC,CAAC,SAAS,MAAM,EAAE,SAAS,OAAO,GAAG;AACxC,aAAO,eAAe,KAAK,4CAA4C;AAAA,IACzE;AAGA,QACE,CAAC,WAAW,MACZ,CAAC,WAAW,eACZ,CAAC,WAAW,aACZ,CAAC,WAAW,SACZ;AACA,aAAO;AAAA,QACL;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAGA,UAAM,cAAc,MAAM,iBAAiB,QAAQ,WAAW,IAAI,MAAM;AACxE,QAAI,CAAC,aAAa;AAChB,aAAO,eAAe,KAAK,eAAe;AAAA,IAC5C;AAGA,UAAM,SAAS,oBAAoB,YAAY;AAAA,MAC7C;AAAA,MACA;AAAA,IACF,CAAC;AAED,WAAO,cAAc,KAAK,MAAM;AAAA,EAClC,SAAS,OAAO;AACd,UAAM,IAAI,MAAM,iCAAiC,KAAK,EAAE;AAAA,EAC1D;AACF;AAzEe;AA2Ef,eAAe,iBACb,SACA,cACA,QAC0C;AAC1C,MAAI;AACF,WAAO,QAAQ,QAAQ,MAAM;AAAA,EAC/B,SAAS,OAAO;AACd,YAAQ;AAAA,MACN,kCAAkC,YAAY,YAAY,MAAM;AAAA,MAChE;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACF;AAde;",
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 allItems = response.Items || [];\n\n // Filter out settings record and get only flow configs\n const flowConfigs = allItems.filter((item) => item.id !== 'application-settings');\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
  }
@@ -97,7 +97,7 @@ var handler = /* @__PURE__ */ __name(async (event) => {
97
97
  logger.info("GetConfig Lambda invoked", { event });
98
98
  try {
99
99
  const configId = event?.Details?.Parameters?.id ?? event.id;
100
- const language = event?.Details?.Parameters?.lang ?? event?.Details?.ContactData?.Attributes?.lang ?? event.lang ?? "en-US";
100
+ const language = event?.Details?.Parameters?.lang ?? event?.Details?.ContactData?.LanguageCode ?? event.lang ?? "en-US";
101
101
  const channel = (event?.Details?.ContactData?.Channel?.toString() ?? event.channel ?? "voice").toLowerCase();
102
102
  if (!configId) {
103
103
  throw new Error("Missing required parameter: id");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../backend/GetConfig.ts", "../../../backend/shared/logger.ts", "../../../backend/shared/transformFlowConfig.ts"],
4
- "sourcesContent": ["import { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';\nimport { logger } from './shared/logger';\nimport { FlowConfig } from './shared/models';\nimport { transformFlowConfig } from './shared/transformFlowConfig';\n\nconst client = new DynamoDBClient({ region: process.env.AWS_REGION });\nconst docClient = DynamoDBDocumentClient.from(client);\n\ninterface GetConfigEvent {\n // Connect Event Structure\n Details: {\n Parameters: {\n id: string;\n lang?: string;\n };\n ContactData: {\n Channel: string;\n Attributes: {\n lang?: string;\n };\n };\n };\n\n // Override Structure\n id: string;\n lang?: string;\n channel?: 'voice' | 'chat';\n}\n\nexport const handler = async (\n event: Partial<GetConfigEvent>\n): Promise<Record<string, string>> => {\n logger.info('GetConfig Lambda invoked', { event });\n\n try {\n // Extract parameters with defaults\n const configId = event?.Details?.Parameters?.id ?? event.id;\n const language =\n event?.Details?.Parameters?.lang ??\n event?.Details?.ContactData?.Attributes?.lang ??\n event.lang ??\n 'en-US';\n const channel = (\n event?.Details?.ContactData?.Channel?.toString() ??\n event.channel ??\n 'voice'\n ).toLowerCase();\n\n if (!configId) {\n throw new Error('Missing required parameter: id');\n }\n\n logger.info('Retrieving flow config', { configId, language, channel });\n\n // Get config from DynamoDB\n const command = new GetCommand({\n TableName: process.env.FLOW_CONFIGS_TABLE_NAME!,\n Key: { id: configId },\n });\n\n const response = await docClient.send(command);\n\n if (!response.Item) {\n logger.warn('Flow config not found', { configId });\n throw new Error(`Flow config with id ${configId} not found`);\n }\n\n const config = response.Item as FlowConfig;\n logger.debug('Retrieved flow config', { config });\n\n // Use shared transformation function\n const result = transformFlowConfig(config, {\n language,\n channel: channel as 'voice' | 'chat',\n });\n\n return result;\n } catch (error) {\n logger.error('Error in GetConfig Lambda', error as Error);\n throw error;\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", "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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",
4
+ "sourcesContent": ["import { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';\nimport { logger } from './shared/logger';\nimport { FlowConfig } from './shared/models';\nimport { transformFlowConfig } from './shared/transformFlowConfig';\n\nconst client = new DynamoDBClient({ region: process.env.AWS_REGION });\nconst docClient = DynamoDBDocumentClient.from(client);\n\ninterface GetConfigEvent {\n // Connect Event Structure\n Details: {\n Parameters: {\n id: string;\n lang?: string;\n };\n ContactData: {\n Channel: string;\n LanguageCode?: string;\n };\n };\n\n // Override Structure\n id: string;\n lang?: string;\n channel?: 'voice' | 'chat';\n}\n\nexport const handler = async (\n event: Partial<GetConfigEvent>\n): Promise<Record<string, string>> => {\n logger.info('GetConfig Lambda invoked', { event });\n\n try {\n // Extract parameters with defaults\n const configId = event?.Details?.Parameters?.id ?? event.id;\n const language =\n event?.Details?.Parameters?.lang ??\n event?.Details?.ContactData?.LanguageCode ??\n event.lang ??\n 'en-US';\n const channel = (\n event?.Details?.ContactData?.Channel?.toString() ??\n event.channel ??\n 'voice'\n ).toLowerCase();\n\n if (!configId) {\n throw new Error('Missing required parameter: id');\n }\n\n logger.info('Retrieving flow config', { configId, language, channel });\n\n // Get config from DynamoDB\n const command = new GetCommand({\n TableName: process.env.FLOW_CONFIGS_TABLE_NAME!,\n Key: { id: configId },\n });\n\n const response = await docClient.send(command);\n\n if (!response.Item) {\n logger.warn('Flow config not found', { configId });\n throw new Error(`Flow config with id ${configId} not found`);\n }\n\n const config = response.Item as FlowConfig;\n logger.debug('Retrieved flow config', { config });\n\n // Use shared transformation function\n const result = transformFlowConfig(config, {\n language,\n channel: channel as 'voice' | 'chat',\n });\n\n return result;\n } catch (error) {\n logger.error('Error in GetConfig Lambda', error as Error);\n throw error;\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", "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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",
6
6
  "names": []
7
7
  }
@@ -122,7 +122,8 @@ var handler = /* @__PURE__ */ __name(async (event, context) => {
122
122
  response = {
123
123
  region: env.AWS_REGION ?? "us-east-1",
124
124
  userPoolId: env.userPoolId,
125
- clientId: outputs.outputs.UserPoolClientId
125
+ clientId: outputs.outputs.UserPoolClientId,
126
+ branding: env.branding === "true" ? true : false
126
127
  };
127
128
  }
128
129
  return respondObject(200, response);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../backend/Init.ts", "../../../backend/shared/logger.ts", "../../../backend/shared/respond.ts", "../../../backend/shared/snsClient.ts", "../../../backend/shared/getVar.ts"],
4
- "sourcesContent": ["import {\n APIGatewayProxyEvent,\n APIGatewayProxyStructuredResultV2,\n Context,\n} from 'aws-lambda';\nimport {\n CloudFormationClient,\n DescribeStacksCommand,\n} from '@aws-sdk/client-cloudformation';\nimport { InitEnv } from '../infrastructure/api/Init/Init.interface';\nimport { InitResponse } from './shared/models';\nimport { logEvent } from './shared/logger';\nimport { respondError, respondObject } from './shared/respond';\nimport { sendError } from './shared/snsClient';\n\nconst env = process.env as unknown as InitEnv;\n\nconst cloudformationClient = new CloudFormationClient();\n\n/**\n * Cache response between invocations of the handler\n */\nlet response: InitResponse | undefined = undefined;\n\n/**\n * Due to a CDK circular dependency, we cannot pass the clientId by reference to the CDK Construct, we must look it up from stack outputs.\n */\nconst getStackOutputs = async (\n stackName: string\n): Promise<{ status: string; outputs: Record<string, string> }> => {\n const stacks = await cloudformationClient.send(\n new DescribeStacksCommand({ StackName: stackName })\n );\n const stack = stacks.Stacks?.pop();\n if (!stack) {\n throw new Error(`Could not locate stack named ${env.stackName}`);\n }\n const outputs: Record<string, string> = {};\n stack.Outputs?.forEach(({ OutputKey = '', OutputValue = '' }) => {\n if (OutputKey) {\n outputs[OutputKey] = OutputValue;\n }\n });\n return {\n status: stack.StackStatus ?? '',\n outputs,\n };\n};\n\nexport const handler = async (\n event?: APIGatewayProxyEvent,\n context?: Context\n): Promise<APIGatewayProxyStructuredResultV2> => {\n logEvent(event, context);\n try {\n if (!response) {\n const outputs = await getStackOutputs(env.stackName);\n response = {\n region: env.AWS_REGION ?? 'us-east-1',\n userPoolId: env.userPoolId,\n clientId: outputs.outputs.UserPoolClientId,\n };\n }\n\n return respondObject(200, response);\n } catch (error) {\n await sendError('Unhandled Error: api/init', error as Error);\n return respondError(error);\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"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {\n APIGatewayProxyEvent,\n APIGatewayProxyStructuredResultV2,\n Context,\n} from 'aws-lambda';\nimport {\n CloudFormationClient,\n DescribeStacksCommand,\n} from '@aws-sdk/client-cloudformation';\nimport { InitEnv } from '../infrastructure/api/Init/Init.interface';\nimport { InitResponse } from './shared/models';\nimport { logEvent } from './shared/logger';\nimport { respondError, respondObject } from './shared/respond';\nimport { sendError } from './shared/snsClient';\n\nconst env = process.env as unknown as InitEnv;\n\nconst cloudformationClient = new CloudFormationClient();\n\n/**\n * Cache response between invocations of the handler\n */\nlet response: InitResponse | undefined = undefined;\n\n/**\n * Due to a CDK circular dependency, we cannot pass the clientId by reference to the CDK Construct, we must look it up from stack outputs.\n */\nconst getStackOutputs = async (\n stackName: string\n): Promise<{ status: string; outputs: Record<string, string> }> => {\n const stacks = await cloudformationClient.send(\n new DescribeStacksCommand({ StackName: stackName })\n );\n const stack = stacks.Stacks?.pop();\n if (!stack) {\n throw new Error(`Could not locate stack named ${env.stackName}`);\n }\n const outputs: Record<string, string> = {};\n stack.Outputs?.forEach(({ OutputKey = '', OutputValue = '' }) => {\n if (OutputKey) {\n outputs[OutputKey] = OutputValue;\n }\n });\n return {\n status: stack.StackStatus ?? '',\n outputs,\n };\n};\n\nexport const handler = async (\n event?: APIGatewayProxyEvent,\n context?: Context\n): Promise<APIGatewayProxyStructuredResultV2> => {\n logEvent(event, context);\n try {\n if (!response) {\n const outputs = await getStackOutputs(env.stackName);\n response = {\n region: env.AWS_REGION ?? 'us-east-1',\n userPoolId: env.userPoolId,\n clientId: outputs.outputs.UserPoolClientId,\n branding: env.branding === 'true' ? true : false,\n };\n }\n\n return respondObject(200, response);\n } catch (error) {\n await sendError('Unhandled Error: api/init', error as Error);\n return respondError(error);\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"],
5
+ "mappings": "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",
6
6
  "names": ["error"]
7
7
  }