@cxbuilder/flow-config 1.1.0 → 2.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.
Files changed (33) hide show
  1. package/.jsii +151 -66
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +8 -5
  4. package/dist/backend/FlowConfig/index.js +2 -1
  5. package/dist/backend/FlowConfig/index.js.map +2 -2
  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 +39 -15
  16. package/dist/infrastructure/FlowConfigStack.js +32 -20
  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/package.json +1 -1
  33. package/dist/backend/Static/static/assets/index-FHwnAA8f.js +0 -61
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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", "../../../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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",
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
  }
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // backend/Settings.ts
22
+ var Settings_exports = {};
23
+ __export(Settings_exports, {
24
+ handler: () => handler
25
+ });
26
+ module.exports = __toCommonJS(Settings_exports);
27
+ var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
28
+ var import_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
29
+
30
+ // backend/shared/logger.ts
31
+ var import_logger = require("@aws-lambda-powertools/logger");
32
+ var logger = new import_logger.Logger();
33
+
34
+ // backend/shared/respond.ts
35
+ var respond = /* @__PURE__ */ __name((statusCode, body) => ({
36
+ statusCode,
37
+ body
38
+ }), "respond");
39
+ var respondObject = /* @__PURE__ */ __name((statusCode, obj) => respond(statusCode, JSON.stringify(obj)), "respondObject");
40
+
41
+ // backend/shared/permissions.ts
42
+ var COGNITO_GROUPS = {
43
+ ADMIN: "FlowConfigAdmin",
44
+ EDIT: "FlowConfigEdit",
45
+ READ: "FlowConfigRead"
46
+ };
47
+ function extractCognitoGroups(claims) {
48
+ const groupsClaim = claims["cognito:groups"];
49
+ if (!groupsClaim) {
50
+ return [];
51
+ }
52
+ if (typeof groupsClaim === "string") {
53
+ return groupsClaim.split(",").map((group) => group.trim());
54
+ }
55
+ if (Array.isArray(groupsClaim)) {
56
+ return groupsClaim;
57
+ }
58
+ return [];
59
+ }
60
+ __name(extractCognitoGroups, "extractCognitoGroups");
61
+ function getAccessLevel(claims) {
62
+ const userGroups = extractCognitoGroups(claims);
63
+ if (userGroups.includes(COGNITO_GROUPS.ADMIN)) {
64
+ return "Full";
65
+ }
66
+ if (userGroups.includes(COGNITO_GROUPS.EDIT)) {
67
+ return "Edit";
68
+ }
69
+ if (userGroups.includes(COGNITO_GROUPS.READ)) {
70
+ return "Read";
71
+ }
72
+ return null;
73
+ }
74
+ __name(getAccessLevel, "getAccessLevel");
75
+
76
+ // backend/Settings.ts
77
+ var client = new import_client_dynamodb.DynamoDBClient({ region: process.env.AWS_REGION });
78
+ var docClient = import_lib_dynamodb.DynamoDBDocumentClient.from(client);
79
+ var env = process.env;
80
+ var SETTINGS_ITEM_ID = "application-settings";
81
+ var DEFAULT_SETTINGS = {
82
+ locales: [
83
+ {
84
+ code: "en-US",
85
+ name: "English (US)",
86
+ voices: ["Joanna"]
87
+ }
88
+ ]
89
+ };
90
+ function validateSettings(settings) {
91
+ if (!settings || typeof settings !== "object") {
92
+ return false;
93
+ }
94
+ if (!Array.isArray(settings.locales)) {
95
+ return false;
96
+ }
97
+ for (const locale of settings.locales) {
98
+ if (!locale || typeof locale !== "object") {
99
+ return false;
100
+ }
101
+ if (!locale.code || typeof locale.code !== "string") {
102
+ return false;
103
+ }
104
+ if (!locale.name || typeof locale.name !== "string") {
105
+ return false;
106
+ }
107
+ if (!Array.isArray(locale.voices)) {
108
+ return false;
109
+ }
110
+ for (const voice of locale.voices) {
111
+ if (typeof voice !== "string") {
112
+ return false;
113
+ }
114
+ }
115
+ const languageCodePattern = /^([a-z]{2,3}(-[A-Z]{2})?(-[A-Z]{3})?|arb)$/;
116
+ if (!languageCodePattern.test(locale.code)) {
117
+ return false;
118
+ }
119
+ }
120
+ return true;
121
+ }
122
+ __name(validateSettings, "validateSettings");
123
+ async function getSettings() {
124
+ try {
125
+ const command = new import_lib_dynamodb.GetCommand({
126
+ TableName: env.TABLE_NAME,
127
+ Key: { id: SETTINGS_ITEM_ID }
128
+ });
129
+ const response = await docClient.send(command);
130
+ if (!response.Item) {
131
+ logger.info("No settings found, returning default settings");
132
+ return DEFAULT_SETTINGS;
133
+ }
134
+ const item = response.Item;
135
+ return item.settings;
136
+ } catch (error) {
137
+ logger.error("Error retrieving settings from DynamoDB", error);
138
+ throw error;
139
+ }
140
+ }
141
+ __name(getSettings, "getSettings");
142
+ async function saveSettings(settings, userId) {
143
+ try {
144
+ const item = {
145
+ id: SETTINGS_ITEM_ID,
146
+ settings,
147
+ lastModified: (/* @__PURE__ */ new Date()).toISOString(),
148
+ lastModifiedBy: userId
149
+ };
150
+ const command = new import_lib_dynamodb.PutCommand({
151
+ TableName: env.TABLE_NAME,
152
+ Item: item
153
+ });
154
+ await docClient.send(command);
155
+ logger.info("Settings saved successfully", {
156
+ userId,
157
+ settingsCount: settings.locales.length
158
+ });
159
+ return settings;
160
+ } catch (error) {
161
+ logger.error("Error saving settings to DynamoDB", error);
162
+ throw error;
163
+ }
164
+ }
165
+ __name(saveSettings, "saveSettings");
166
+ async function handleGetSettings() {
167
+ try {
168
+ const settings = await getSettings();
169
+ return respondObject(200, settings);
170
+ } catch (error) {
171
+ logger.error("Error handling GET settings request", error);
172
+ return respondObject(500, {
173
+ code: "INTERNAL_ERROR",
174
+ message: "Failed to retrieve settings"
175
+ });
176
+ }
177
+ }
178
+ __name(handleGetSettings, "handleGetSettings");
179
+ async function handlePostSettings(event) {
180
+ try {
181
+ const claims = event.requestContext.authorizer?.claims || {};
182
+ const accessLevel = getAccessLevel(claims);
183
+ if (accessLevel !== "Full") {
184
+ logger.warn("Access denied - admin access required", {
185
+ userId: claims.sub,
186
+ accessLevel
187
+ });
188
+ return respondObject(403, {
189
+ code: "FORBIDDEN",
190
+ message: "Admin access required"
191
+ });
192
+ }
193
+ if (!event.body) {
194
+ return respondObject(400, {
195
+ code: "INVALID_REQUEST",
196
+ message: "Request body is required"
197
+ });
198
+ }
199
+ let settings;
200
+ try {
201
+ settings = JSON.parse(event.body);
202
+ } catch (error) {
203
+ return respondObject(400, {
204
+ code: "INVALID_JSON",
205
+ message: "Invalid JSON in request body"
206
+ });
207
+ }
208
+ if (!validateSettings(settings)) {
209
+ return respondObject(400, {
210
+ code: "INVALID_SETTINGS",
211
+ message: "Invalid settings format"
212
+ });
213
+ }
214
+ const userId = event.requestContext.authorizer?.claims?.sub || "unknown";
215
+ const savedSettings = await saveSettings(settings, userId);
216
+ return respondObject(200, savedSettings);
217
+ } catch (error) {
218
+ logger.error("Error handling POST settings request", error);
219
+ return respondObject(500, {
220
+ code: "INTERNAL_ERROR",
221
+ message: "Failed to save settings"
222
+ });
223
+ }
224
+ }
225
+ __name(handlePostSettings, "handlePostSettings");
226
+ var handler = /* @__PURE__ */ __name(async (event) => {
227
+ logger.info("Settings handler invoked", {
228
+ httpMethod: event.httpMethod,
229
+ path: event.path
230
+ });
231
+ try {
232
+ switch (event.httpMethod) {
233
+ case "GET":
234
+ return await handleGetSettings();
235
+ case "POST":
236
+ return await handlePostSettings(event);
237
+ default:
238
+ return respondObject(405, {
239
+ code: "METHOD_NOT_ALLOWED",
240
+ message: `Method ${event.httpMethod} not allowed`
241
+ });
242
+ }
243
+ } catch (error) {
244
+ logger.error("Unexpected error in settings handler", error);
245
+ return respondObject(500, {
246
+ code: "INTERNAL_ERROR",
247
+ message: "Internal server error"
248
+ });
249
+ }
250
+ }, "handler");
251
+ // Annotate the CommonJS export names for ESM import in node:
252
+ 0 && (module.exports = {
253
+ handler
254
+ });
255
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../backend/Settings.ts", "../../../backend/shared/logger.ts", "../../../backend/shared/respond.ts", "../../../backend/shared/permissions.ts"],
4
+ "sourcesContent": ["import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';\nimport { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport {\n DynamoDBDocumentClient,\n GetCommand,\n PutCommand,\n} from '@aws-sdk/lib-dynamodb';\nimport { logger } from './shared/logger';\nimport { respondObject } from './shared/respond';\nimport { getAccessLevel } from './shared/permissions';\nimport { Settings } from './shared/models';\nimport { SettingsEnv } from '../infrastructure/api/Settings/Settings.interface';\n\nconst client = new DynamoDBClient({ region: process.env.AWS_REGION });\nconst docClient = DynamoDBDocumentClient.from(client);\n\nconst env = process.env as unknown as SettingsEnv;\n\n// Settings item ID - we use a single item to store all settings\nconst SETTINGS_ITEM_ID = 'application-settings';\n\n// Data structures - using shared types from models\n\ninterface SettingsItem {\n id: string;\n settings: Settings;\n lastModified: string;\n lastModifiedBy: string;\n}\n\n// Default settings\nconst DEFAULT_SETTINGS: Settings = {\n locales: [\n {\n code: 'en-US',\n name: 'English (US)',\n voices: ['Joanna'],\n },\n ],\n};\n\n/**\n * Validates the settings object\n */\nfunction validateSettings(settings: any): settings is Settings {\n if (!settings || typeof settings !== 'object') {\n return false;\n }\n\n if (!Array.isArray(settings.locales)) {\n return false;\n }\n\n // Validate each locale\n for (const locale of settings.locales) {\n if (!locale || typeof locale !== 'object') {\n return false;\n }\n\n if (!locale.code || typeof locale.code !== 'string') {\n return false;\n }\n\n if (!locale.name || typeof locale.name !== 'string') {\n return false;\n }\n\n if (!Array.isArray(locale.voices)) {\n return false;\n }\n\n // Validate voice IDs are strings\n for (const voice of locale.voices) {\n if (typeof voice !== 'string') {\n return false;\n }\n }\n\n // Validate language code format (Amazon Polly format)\n const languageCodePattern = /^([a-z]{2,3}(-[A-Z]{2})?(-[A-Z]{3})?|arb)$/;\n if (!languageCodePattern.test(locale.code)) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Get settings from DynamoDB\n */\nasync function getSettings(): Promise<Settings> {\n try {\n const command = new GetCommand({\n TableName: env.TABLE_NAME,\n Key: { id: SETTINGS_ITEM_ID },\n });\n\n const response = await docClient.send(command);\n\n if (!response.Item) {\n logger.info('No settings found, returning default settings');\n return DEFAULT_SETTINGS;\n }\n\n const item = response.Item as SettingsItem;\n return item.settings;\n } catch (error) {\n logger.error('Error retrieving settings from DynamoDB', error as Error);\n throw error;\n }\n}\n\n/**\n * Save settings to DynamoDB\n */\nasync function saveSettings(\n settings: Settings,\n userId: string\n): Promise<Settings> {\n try {\n const item: SettingsItem = {\n id: SETTINGS_ITEM_ID,\n settings,\n lastModified: new Date().toISOString(),\n lastModifiedBy: userId,\n };\n\n const command = new PutCommand({\n TableName: env.TABLE_NAME,\n Item: item,\n });\n\n await docClient.send(command);\n logger.info('Settings saved successfully', {\n userId,\n settingsCount: settings.locales.length,\n });\n\n return settings;\n } catch (error) {\n logger.error('Error saving settings to DynamoDB', error as Error);\n throw error;\n }\n}\n\n/**\n * Handle GET /api/settings\n */\nasync function handleGetSettings(): Promise<APIGatewayProxyResult> {\n try {\n const settings = await getSettings();\n\n return respondObject(200, settings);\n } catch (error) {\n logger.error('Error handling GET settings request', error as Error);\n return respondObject(500, {\n code: 'INTERNAL_ERROR',\n message: 'Failed to retrieve settings',\n });\n }\n}\n\n/**\n * Handle POST /api/settings\n */\nasync function handlePostSettings(\n event: APIGatewayProxyEvent\n): Promise<APIGatewayProxyResult> {\n try {\n // Get user claims and check access level\n const claims = event.requestContext.authorizer?.claims || {};\n const accessLevel = getAccessLevel(claims);\n\n // Check if user has admin access\n if (accessLevel !== 'Full') {\n logger.warn('Access denied - admin access required', {\n userId: claims.sub,\n accessLevel,\n });\n return respondObject(403, {\n code: 'FORBIDDEN',\n message: 'Admin access required',\n });\n }\n\n // Parse request body\n if (!event.body) {\n return respondObject(400, {\n code: 'INVALID_REQUEST',\n message: 'Request body is required',\n });\n }\n\n let settings: Settings;\n try {\n settings = JSON.parse(event.body);\n } catch (error) {\n return respondObject(400, {\n code: 'INVALID_JSON',\n message: 'Invalid JSON in request body',\n });\n }\n\n // Validate settings\n if (!validateSettings(settings)) {\n return respondObject(400, {\n code: 'INVALID_SETTINGS',\n message: 'Invalid settings format',\n });\n }\n\n // Get user ID from Cognito claims\n const userId = event.requestContext.authorizer?.claims?.sub || 'unknown';\n\n // Save settings\n const savedSettings = await saveSettings(settings, userId);\n\n return respondObject(200, savedSettings);\n } catch (error) {\n logger.error('Error handling POST settings request', error as Error);\n return respondObject(500, {\n code: 'INTERNAL_ERROR',\n message: 'Failed to save settings',\n });\n }\n}\n\n/**\n * Main Lambda handler\n */\nexport const handler = async (\n event: APIGatewayProxyEvent\n): Promise<APIGatewayProxyResult> => {\n logger.info('Settings handler invoked', {\n httpMethod: event.httpMethod,\n path: event.path,\n });\n\n try {\n // Route based on HTTP method\n switch (event.httpMethod) {\n case 'GET':\n return await handleGetSettings();\n case 'POST':\n return await handlePostSettings(event);\n default:\n return respondObject(405, {\n code: 'METHOD_NOT_ALLOWED',\n message: `Method ${event.httpMethod} not allowed`,\n });\n }\n } catch (error) {\n logger.error('Unexpected error in settings handler', error as Error);\n return respondObject(500, {\n code: 'INTERNAL_ERROR',\n message: 'Internal server error',\n });\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", "/**\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
+ "names": []
7
+ }