belt-connect 0.2.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +121 -0
- data/LICENSE +21 -0
- data/README.md +169 -0
- data/lib/belt/connect/configuration.rb +35 -0
- data/lib/belt/connect/snippet_parser.rb +129 -0
- data/lib/belt/connect/templates/config/widgets-README.md.erb +63 -0
- data/lib/belt/connect/templates/frontend/ConnectWidget.jsx.erb +116 -0
- data/lib/belt/connect/templates/frontend/widgets.js.erb +59 -0
- data/lib/belt/connect/templates/terraform/main.tf.erb +323 -0
- data/lib/belt/connect/templates/terraform/outputs.tf.erb +76 -0
- data/lib/belt/connect/templates/terraform/variables.tf.erb +100 -0
- data/lib/belt/connect/version.rb +7 -0
- data/lib/belt/connect.rb +35 -0
- data/lib/belt/generators/connect/add_widget.rb +319 -0
- data/lib/belt/generators/connect_generator.rb +322 -0
- data/lib/belt-connect.rb +3 -0
- metadata +110 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<%# belt-connect :: frontend/src/connect/widgets.js -%>
|
|
2
|
+
// Amazon Connect widget registry.
|
|
3
|
+
// Generated by belt-connect. This is the single place that declares every
|
|
4
|
+
// hosted chat widget the app knows about — convention over configuration.
|
|
5
|
+
//
|
|
6
|
+
// Each widget's identity (id, snippetId, widgetUrl) comes straight from the
|
|
7
|
+
// `<script>` snippet the Connect console hands you. Run
|
|
8
|
+
// `belt generate connect add-widget <name> <snippet-file>` to parse a pasted
|
|
9
|
+
// snippet into this file so the base64 snippetId is never retyped by hand
|
|
10
|
+
// (that's what caused the right-instance/wrong-widget 403).
|
|
11
|
+
//
|
|
12
|
+
// The snippetId is a secret, so it's read from a Vite env var at build time;
|
|
13
|
+
// everything else is readable config that lives in the repo. Brand color and
|
|
14
|
+
// typeface default to the belt-connect config but can be overridden per widget.
|
|
15
|
+
//
|
|
16
|
+
// Add a second widget by dropping another entry in the map — no new component,
|
|
17
|
+
// no new env-var sprawl:
|
|
18
|
+
//
|
|
19
|
+
// sales: {
|
|
20
|
+
// id: '...',
|
|
21
|
+
// snippetId: import.meta.env.VITE_CONNECT_SALES_SNIPPET_ID,
|
|
22
|
+
// widgetUrl: 'https://<instance>.my.connect.aws/.../amazon-connect-chat-interface-client.js',
|
|
23
|
+
// styles: { iconType: 'CHAT', brandColor: '#0ea5e9' },
|
|
24
|
+
// }
|
|
25
|
+
|
|
26
|
+
const BRAND_COLOR = "<%= @brand_color %>";
|
|
27
|
+
const TYPEFACE = "<%= @typeface %>";
|
|
28
|
+
|
|
29
|
+
export const widgets = {
|
|
30
|
+
support: {
|
|
31
|
+
// Widget instance id (4th arg of the Connect bootstrap snippet).
|
|
32
|
+
id: import.meta.env.VITE_CONNECT_SUPPORT_WIDGET_ID || "",
|
|
33
|
+
// Secret — stays in env/SSM, never committed.
|
|
34
|
+
snippetId: import.meta.env.VITE_CONNECT_SUPPORT_SNIPPET_ID,
|
|
35
|
+
// Hosted widget loader URL (same per Connect instance).
|
|
36
|
+
widgetUrl: import.meta.env.VITE_CONNECT_SUPPORT_WIDGET_URL,
|
|
37
|
+
styles: {
|
|
38
|
+
iconType: "CHAT",
|
|
39
|
+
brandColor: BRAND_COLOR,
|
|
40
|
+
typeface: TYPEFACE,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Defaults applied to any widget that doesn't override them.
|
|
46
|
+
export const widgetDefaults = {
|
|
47
|
+
brandColor: BRAND_COLOR,
|
|
48
|
+
typeface: TYPEFACE,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function getWidget(name) {
|
|
52
|
+
const widget = widgets[name];
|
|
53
|
+
if (!widget) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`[belt-connect] Unknown widget "${name}". Known widgets: ${Object.keys(widgets).join(", ") || "(none)"}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return widget;
|
|
59
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
<%# belt-connect :: infrastructure/modules/connect/main.tf -%>
|
|
2
|
+
# Amazon Connect (Connect Customer) module.
|
|
3
|
+
# Generated by belt-connect for the "<%= @app_name %>" app.
|
|
4
|
+
# Extracted and generalized from Stowzilla's standalone connect-infrastructure.
|
|
5
|
+
|
|
6
|
+
terraform {
|
|
7
|
+
required_providers {
|
|
8
|
+
aws = { source = "hashicorp/aws" }
|
|
9
|
+
null = { source = "hashicorp/null" }
|
|
10
|
+
random = { source = "hashicorp/random" }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
locals {
|
|
15
|
+
name_prefix = "${var.app_name}-${var.environment}"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# --- Connect Instance ---
|
|
19
|
+
# Note: the Connect service-linked role (AWSServiceRoleForAmazonConnect) is
|
|
20
|
+
# created automatically by AWS when the first instance is provisioned. It's a
|
|
21
|
+
# one-per-account resource and should not be managed by per-environment Terraform.
|
|
22
|
+
#
|
|
23
|
+
# identity_management_type is a create-time setting: changing it replaces the
|
|
24
|
+
# instance (and everything in it — flows, queues, users, hosted widgets).
|
|
25
|
+
resource "aws_connect_instance" "this" {
|
|
26
|
+
identity_management_type = var.identity_management_type
|
|
27
|
+
instance_alias = var.instance_alias
|
|
28
|
+
inbound_calls_enabled = true
|
|
29
|
+
outbound_calls_enabled = false
|
|
30
|
+
contact_flow_logs_enabled = true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# --- Approved Origins ---
|
|
34
|
+
# Domains allowed to embed the Connect chat widget.
|
|
35
|
+
# No native Terraform resource exists, so we use local-exec.
|
|
36
|
+
resource "null_resource" "approved_origins" {
|
|
37
|
+
for_each = toset(var.approved_origins)
|
|
38
|
+
|
|
39
|
+
provisioner "local-exec" {
|
|
40
|
+
command = "aws connect associate-approved-origin --instance-id ${aws_connect_instance.this.id} --origin ${each.value}"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
provisioner "local-exec" {
|
|
44
|
+
when = destroy
|
|
45
|
+
command = "aws connect disassociate-approved-origin --instance-id ${self.triggers.instance_id} --origin ${self.triggers.origin} || true"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
triggers = {
|
|
49
|
+
instance_id = aws_connect_instance.this.id
|
|
50
|
+
origin = each.value
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# --- Hours of Operation ---
|
|
55
|
+
resource "aws_connect_hours_of_operation" "business_hours" {
|
|
56
|
+
instance_id = aws_connect_instance.this.id
|
|
57
|
+
name = "${local.name_prefix}-business-hours"
|
|
58
|
+
description = "24/7 availability"
|
|
59
|
+
time_zone = var.timezone
|
|
60
|
+
|
|
61
|
+
dynamic "config" {
|
|
62
|
+
for_each = ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"]
|
|
63
|
+
content {
|
|
64
|
+
day = config.value
|
|
65
|
+
start_time {
|
|
66
|
+
hours = 0
|
|
67
|
+
minutes = 0
|
|
68
|
+
}
|
|
69
|
+
end_time {
|
|
70
|
+
hours = 0
|
|
71
|
+
minutes = 0
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
tags = { Name = "${local.name_prefix}-business-hours" }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# --- Contact Flow ---
|
|
80
|
+
# Routes to queue during hours, plays message and disconnects off-hours.
|
|
81
|
+
# Off-hours messages are captured as missed contacts in Connect reporting.
|
|
82
|
+
resource "aws_connect_contact_flow" "chat" {
|
|
83
|
+
instance_id = aws_connect_instance.this.id
|
|
84
|
+
name = "${local.name_prefix}-chat-flow"
|
|
85
|
+
description = "Chat routing with off-hours message queuing"
|
|
86
|
+
type = "CONTACT_FLOW"
|
|
87
|
+
|
|
88
|
+
content = jsonencode({
|
|
89
|
+
Version = "2019-10-30"
|
|
90
|
+
StartAction = "set-queue"
|
|
91
|
+
Actions = [
|
|
92
|
+
{
|
|
93
|
+
Identifier = "set-queue"
|
|
94
|
+
Type = "UpdateContactTargetQueue"
|
|
95
|
+
Parameters = { QueueId = aws_connect_queue.chat.queue_id }
|
|
96
|
+
Transitions = {
|
|
97
|
+
NextAction = "transfer-to-queue"
|
|
98
|
+
Conditions = []
|
|
99
|
+
Errors = [{ NextAction = "disconnect", ErrorType = "NoMatchingError" }]
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
Identifier = "transfer-to-queue"
|
|
104
|
+
Type = "TransferContactToQueue"
|
|
105
|
+
Parameters = {}
|
|
106
|
+
Transitions = {
|
|
107
|
+
NextAction = "disconnect"
|
|
108
|
+
Conditions = []
|
|
109
|
+
Errors = [
|
|
110
|
+
{ NextAction = "disconnect", ErrorType = "QueueAtCapacity" },
|
|
111
|
+
{ NextAction = "disconnect", ErrorType = "NoMatchingError" }
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
Identifier = "disconnect"
|
|
117
|
+
Type = "DisconnectParticipant"
|
|
118
|
+
Parameters = {}
|
|
119
|
+
Transitions = {}
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
tags = { Name = "${local.name_prefix}-chat-flow" }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# --- Queue ---
|
|
128
|
+
resource "aws_connect_queue" "chat" {
|
|
129
|
+
instance_id = aws_connect_instance.this.id
|
|
130
|
+
name = "${local.name_prefix}-chat-queue"
|
|
131
|
+
description = "Queue for customer chat contacts"
|
|
132
|
+
hours_of_operation_id = aws_connect_hours_of_operation.business_hours.hours_of_operation_id
|
|
133
|
+
|
|
134
|
+
tags = { Name = "${local.name_prefix}-chat-queue" }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# --- Routing Profile ---
|
|
138
|
+
resource "aws_connect_routing_profile" "chat_agents" {
|
|
139
|
+
instance_id = aws_connect_instance.this.id
|
|
140
|
+
name = "${local.name_prefix}-chat-agents"
|
|
141
|
+
description = "Routing profile for chat agents"
|
|
142
|
+
default_outbound_queue_id = aws_connect_queue.chat.queue_id
|
|
143
|
+
|
|
144
|
+
media_concurrencies {
|
|
145
|
+
channel = "CHAT"
|
|
146
|
+
concurrency = 3
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
queue_configs {
|
|
150
|
+
channel = "CHAT"
|
|
151
|
+
delay = 0
|
|
152
|
+
priority = 1
|
|
153
|
+
queue_id = aws_connect_queue.chat.queue_id
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
tags = { Name = "${local.name_prefix}-chat-agents" }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# --- Users (CONNECT_MANAGED only) ---
|
|
160
|
+
# A chat only "works" once someone can sign in to the CCP and go Available.
|
|
161
|
+
# With SAML that someone comes from your IdP; with CONNECT_MANAGED we create
|
|
162
|
+
# the logins here, attach them to the chat routing profile, and park each
|
|
163
|
+
# generated password in SSM (SecureString) so nothing secret lands in state
|
|
164
|
+
# outputs or in the repo.
|
|
165
|
+
locals {
|
|
166
|
+
user_security_profiles = distinct(flatten([for u in var.users : u.security_profiles]))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
data "aws_connect_security_profile" "builtin" {
|
|
170
|
+
for_each = toset(local.user_security_profiles)
|
|
171
|
+
instance_id = aws_connect_instance.this.id
|
|
172
|
+
name = each.value
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
resource "random_password" "user" {
|
|
176
|
+
for_each = var.users
|
|
177
|
+
length = 20
|
|
178
|
+
min_upper = 2
|
|
179
|
+
min_lower = 2
|
|
180
|
+
min_numeric = 2
|
|
181
|
+
special = false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
resource "aws_connect_user" "this" {
|
|
185
|
+
for_each = var.users
|
|
186
|
+
|
|
187
|
+
instance_id = aws_connect_instance.this.id
|
|
188
|
+
name = each.key
|
|
189
|
+
password = random_password.user[each.key].result
|
|
190
|
+
routing_profile_id = aws_connect_routing_profile.chat_agents.routing_profile_id
|
|
191
|
+
security_profile_ids = [for p in each.value.security_profiles : data.aws_connect_security_profile.builtin[p].security_profile_id]
|
|
192
|
+
|
|
193
|
+
identity_info {
|
|
194
|
+
first_name = each.value.first_name != "" ? each.value.first_name : each.key
|
|
195
|
+
last_name = each.value.last_name != "" ? each.value.last_name : var.environment
|
|
196
|
+
email = each.value.email != "" ? each.value.email : null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
phone_config {
|
|
200
|
+
phone_type = "SOFT_PHONE"
|
|
201
|
+
auto_accept = false
|
|
202
|
+
after_contact_work_time_limit = 0
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
tags = { Name = "${local.name_prefix}-user-${each.key}" }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
resource "aws_ssm_parameter" "user_password" {
|
|
209
|
+
for_each = var.users
|
|
210
|
+
|
|
211
|
+
name = "/${var.app_name}/${var.environment}/connect/users/${each.key}/password"
|
|
212
|
+
type = "SecureString"
|
|
213
|
+
value = random_password.user[each.key].result
|
|
214
|
+
description = "Amazon Connect password for ${each.key} (${var.environment}) — sign in at https://${var.instance_alias}.my.connect.aws"
|
|
215
|
+
|
|
216
|
+
tags = { Name = "${local.name_prefix}-user-${each.key}-password" }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# --- SSM Parameters for Chat Widget ---
|
|
220
|
+
# Set these after creating the widget in the Connect admin console.
|
|
221
|
+
# The SSM prefix is /<app_name>/<environment>/connect/ — app-scoped, not hard-coded.
|
|
222
|
+
resource "aws_ssm_parameter" "chat_snippet_id" {
|
|
223
|
+
name = "/${var.app_name}/${var.environment}/connect/chat-snippet-id"
|
|
224
|
+
type = "String"
|
|
225
|
+
value = var.chat_snippet_id != "" ? var.chat_snippet_id : "PENDING_CONSOLE_SETUP"
|
|
226
|
+
description = "Amazon Connect chat snippet ID for ${var.environment}"
|
|
227
|
+
|
|
228
|
+
tags = { Name = "${local.name_prefix}-chat-snippet-id" }
|
|
229
|
+
|
|
230
|
+
lifecycle { ignore_changes = [value] }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
resource "aws_ssm_parameter" "chat_widget_url" {
|
|
234
|
+
name = "/${var.app_name}/${var.environment}/connect/chat-widget-url"
|
|
235
|
+
type = "String"
|
|
236
|
+
value = var.chat_widget_url != "" ? var.chat_widget_url : "PENDING_CONSOLE_SETUP"
|
|
237
|
+
description = "Amazon Connect chat widget script URL for ${var.environment}"
|
|
238
|
+
|
|
239
|
+
tags = { Name = "${local.name_prefix}-chat-widget-url" }
|
|
240
|
+
|
|
241
|
+
lifecycle { ignore_changes = [value] }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
resource "aws_ssm_parameter" "chat_widget_element_id" {
|
|
245
|
+
name = "/${var.app_name}/${var.environment}/connect/chat-widget-element-id"
|
|
246
|
+
type = "String"
|
|
247
|
+
value = var.chat_widget_element_id != "" ? var.chat_widget_element_id : "PENDING_CONSOLE_SETUP"
|
|
248
|
+
description = "Amazon Connect chat widget element/instance ID for ${var.environment}"
|
|
249
|
+
|
|
250
|
+
tags = { Name = "${local.name_prefix}-chat-widget-element-id" }
|
|
251
|
+
|
|
252
|
+
lifecycle { ignore_changes = [value] }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# Read the widget params back so outputs reflect the LIVE value (set after the
|
|
256
|
+
# console widget exists via `aws ssm put-parameter`) rather than the apply-time
|
|
257
|
+
# var. The snippetId + script URL + element id are a MATCHED SET and must come
|
|
258
|
+
# from the same widget — see the demo-app README "Chat widget 403" runbook.
|
|
259
|
+
data "aws_ssm_parameter" "chat_snippet_id" {
|
|
260
|
+
name = aws_ssm_parameter.chat_snippet_id.name
|
|
261
|
+
depends_on = [aws_ssm_parameter.chat_snippet_id]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
data "aws_ssm_parameter" "chat_widget_url" {
|
|
265
|
+
name = aws_ssm_parameter.chat_widget_url.name
|
|
266
|
+
depends_on = [aws_ssm_parameter.chat_widget_url]
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
data "aws_ssm_parameter" "chat_widget_element_id" {
|
|
270
|
+
name = aws_ssm_parameter.chat_widget_element_id.name
|
|
271
|
+
depends_on = [aws_ssm_parameter.chat_widget_element_id]
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# --- SAML Federation ---
|
|
275
|
+
# IAM SAML provider and role for Identity Center SSO into the Connect console.
|
|
276
|
+
# Count-gated: only created when a SAML metadata file is supplied.
|
|
277
|
+
data "aws_caller_identity" "current" {}
|
|
278
|
+
|
|
279
|
+
resource "aws_iam_saml_provider" "connect" {
|
|
280
|
+
count = var.saml_metadata_file != "" ? 1 : 0
|
|
281
|
+
name = "${local.name_prefix}-connect-saml"
|
|
282
|
+
saml_metadata_document = file(var.saml_metadata_file)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
resource "aws_iam_role" "connect_saml" {
|
|
286
|
+
count = var.saml_metadata_file != "" ? 1 : 0
|
|
287
|
+
name = "${local.name_prefix}-connect-saml-role"
|
|
288
|
+
|
|
289
|
+
assume_role_policy = jsonencode({
|
|
290
|
+
Version = "2012-10-17"
|
|
291
|
+
Statement = [
|
|
292
|
+
{
|
|
293
|
+
Effect = "Allow"
|
|
294
|
+
Principal = { Federated = aws_iam_saml_provider.connect[0].arn }
|
|
295
|
+
Action = "sts:AssumeRoleWithSAML"
|
|
296
|
+
Condition = {
|
|
297
|
+
StringEquals = {
|
|
298
|
+
"SAML:aud" = "https://signin.aws.amazon.com/saml"
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
]
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
tags = { Name = "${local.name_prefix}-connect-saml-role" }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
resource "aws_iam_role_policy" "connect_saml" {
|
|
309
|
+
count = var.saml_metadata_file != "" ? 1 : 0
|
|
310
|
+
name = "connect-federation"
|
|
311
|
+
role = aws_iam_role.connect_saml[0].id
|
|
312
|
+
|
|
313
|
+
policy = jsonencode({
|
|
314
|
+
Version = "2012-10-17"
|
|
315
|
+
Statement = [
|
|
316
|
+
{
|
|
317
|
+
Effect = "Allow"
|
|
318
|
+
Action = "connect:GetFederationToken"
|
|
319
|
+
Resource = "${aws_connect_instance.this.arn}/user/*"
|
|
320
|
+
}
|
|
321
|
+
]
|
|
322
|
+
})
|
|
323
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<%# belt-connect :: infrastructure/modules/connect/outputs.tf -%>
|
|
2
|
+
output "instance_id" {
|
|
3
|
+
description = "Amazon Connect instance ID"
|
|
4
|
+
value = aws_connect_instance.this.id
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
output "instance_arn" {
|
|
8
|
+
description = "Amazon Connect instance ARN"
|
|
9
|
+
value = aws_connect_instance.this.arn
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
output "admin_console_url" {
|
|
13
|
+
description = "URL for the Amazon Connect admin console"
|
|
14
|
+
value = "https://${var.instance_alias}.my.connect.aws"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
output "ccp_url" {
|
|
18
|
+
description = "Contact Control Panel (CCP) URL — where agents sign in to take chats"
|
|
19
|
+
value = "https://${var.instance_alias}.my.connect.aws/ccp-v2"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
output "identity_management_type" {
|
|
23
|
+
description = "How users sign in: SAML (via IdP) or CONNECT_MANAGED (username + password)"
|
|
24
|
+
value = var.identity_management_type
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
output "user_password_ssm_params" {
|
|
28
|
+
description = "Map of username => SSM SecureString parameter holding that user's generated password (CONNECT_MANAGED only)"
|
|
29
|
+
value = { for name, p in aws_ssm_parameter.user_password : name => p.name }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# --- Chat widget values (matched set, sourced from SSM) ---
|
|
33
|
+
# Wire these into the frontend build via env.yml so the snippet can never go
|
|
34
|
+
# stale in a hand-edited .env. See the demo-app README "Chat widget 403" runbook.
|
|
35
|
+
output "chat_snippet_id" {
|
|
36
|
+
description = "Amazon Connect chat snippet ID (VITE_CONNECT_<NAME>_SNIPPET_ID)"
|
|
37
|
+
value = data.aws_ssm_parameter.chat_snippet_id.value
|
|
38
|
+
# SSM data-source values are sensitive; root outputs that forward this must
|
|
39
|
+
# also set sensitive = true (belt reads it back with `terraform output -json`).
|
|
40
|
+
sensitive = true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
output "chat_widget_url" {
|
|
44
|
+
description = "Amazon Connect chat widget script URL (widgetUrl in the frontend widget registry)"
|
|
45
|
+
value = nonsensitive(data.aws_ssm_parameter.chat_widget_url.value)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
output "chat_widget_element_id" {
|
|
49
|
+
description = "Amazon Connect chat widget element/instance ID (id in the frontend widget registry)"
|
|
50
|
+
value = nonsensitive(data.aws_ssm_parameter.chat_widget_element_id.value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
output "contact_flow_id" {
|
|
54
|
+
description = "Chat contact flow ID"
|
|
55
|
+
value = aws_connect_contact_flow.chat.contact_flow_id
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
output "queue_id" {
|
|
59
|
+
description = "Chat queue ID"
|
|
60
|
+
value = aws_connect_queue.chat.queue_id
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
output "routing_profile_id" {
|
|
64
|
+
description = "Chat agents routing profile ID"
|
|
65
|
+
value = aws_connect_routing_profile.chat_agents.routing_profile_id
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
output "saml_provider_arn" {
|
|
69
|
+
description = "SAML provider ARN for Identity Center federation"
|
|
70
|
+
value = length(aws_iam_saml_provider.connect) > 0 ? aws_iam_saml_provider.connect[0].arn : ""
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
output "saml_role_arn" {
|
|
74
|
+
description = "IAM role ARN for SAML federation into Connect"
|
|
75
|
+
value = length(aws_iam_role.connect_saml) > 0 ? aws_iam_role.connect_saml[0].arn : ""
|
|
76
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<%# belt-connect :: infrastructure/modules/connect/variables.tf -%>
|
|
2
|
+
variable "app_name" {
|
|
3
|
+
description = "Application name prefix (used for resource names and the SSM path)"
|
|
4
|
+
type = string
|
|
5
|
+
default = "<%= @app_name %>"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
variable "environment" {
|
|
9
|
+
description = "Environment name"
|
|
10
|
+
type = string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
variable "aws_region" {
|
|
14
|
+
description = "AWS region"
|
|
15
|
+
type = string
|
|
16
|
+
default = "us-east-1"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
variable "instance_alias" {
|
|
20
|
+
description = "Alias for the Connect instance (must be globally unique)"
|
|
21
|
+
type = string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
variable "identity_management_type" {
|
|
25
|
+
description = <<-EOT
|
|
26
|
+
How agents and admins sign in to this instance.
|
|
27
|
+
SAML — federate from your IdP (pair with saml_metadata_file).
|
|
28
|
+
Nobody can log in until the IdP is wired up; the only
|
|
29
|
+
back door is "Log in for emergency access" in the AWS console.
|
|
30
|
+
CONNECT_MANAGED — usernames + passwords stored in Connect. No IdP needed;
|
|
31
|
+
the simplest choice for demos, sandboxes, and ephemeral envs.
|
|
32
|
+
Pair with the `users` variable to create logins.
|
|
33
|
+
EOT
|
|
34
|
+
type = string
|
|
35
|
+
default = "SAML"
|
|
36
|
+
|
|
37
|
+
validation {
|
|
38
|
+
condition = contains(["SAML", "CONNECT_MANAGED"], var.identity_management_type)
|
|
39
|
+
error_message = "identity_management_type must be \"SAML\" or \"CONNECT_MANAGED\"."
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
variable "users" {
|
|
44
|
+
description = <<-EOT
|
|
45
|
+
Connect-managed logins to create (CONNECT_MANAGED only), keyed by username.
|
|
46
|
+
Every user is attached to the chat routing profile so they can take chats
|
|
47
|
+
from the CCP. A password is generated per user and stored as an SSM
|
|
48
|
+
SecureString at /<app_name>/<environment>/connect/users/<username>/password.
|
|
49
|
+
security_profiles are the built-in Connect profile names
|
|
50
|
+
(Admin, Agent, CallCenterManager, QualityAnalyst).
|
|
51
|
+
EOT
|
|
52
|
+
type = map(object({
|
|
53
|
+
first_name = optional(string, "")
|
|
54
|
+
last_name = optional(string, "")
|
|
55
|
+
email = optional(string, "")
|
|
56
|
+
security_profiles = optional(list(string), ["Agent"])
|
|
57
|
+
}))
|
|
58
|
+
default = {}
|
|
59
|
+
|
|
60
|
+
validation {
|
|
61
|
+
condition = length(var.users) == 0 || var.identity_management_type == "CONNECT_MANAGED"
|
|
62
|
+
error_message = "users can only be created when identity_management_type = \"CONNECT_MANAGED\" (SAML users come from your IdP)."
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
variable "timezone" {
|
|
67
|
+
description = "Timezone for hours of operation"
|
|
68
|
+
type = string
|
|
69
|
+
default = "America/New_York"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
variable "chat_snippet_id" {
|
|
73
|
+
description = "Snippet ID from the Connect admin console chat widget configuration"
|
|
74
|
+
type = string
|
|
75
|
+
default = ""
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
variable "chat_widget_url" {
|
|
79
|
+
description = "Widget script URL from the Connect admin console chat widget configuration"
|
|
80
|
+
type = string
|
|
81
|
+
default = ""
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
variable "chat_widget_element_id" {
|
|
85
|
+
description = "Widget element/instance ID from the Connect admin console chat widget snippet (the id passed as the 4th arg of the snippet IIFE)"
|
|
86
|
+
type = string
|
|
87
|
+
default = ""
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
variable "approved_origins" {
|
|
91
|
+
description = "Domains allowed to embed the Connect chat widget (e.g. https://app.example.com)"
|
|
92
|
+
type = list(string)
|
|
93
|
+
default = []
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
variable "saml_metadata_file" {
|
|
97
|
+
description = "Path to the SAML metadata XML file from Identity Center (empty = no SSO federation)"
|
|
98
|
+
type = string
|
|
99
|
+
default = ""
|
|
100
|
+
}
|
data/lib/belt/connect.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'connect/version'
|
|
4
|
+
require_relative 'connect/configuration'
|
|
5
|
+
|
|
6
|
+
module Belt
|
|
7
|
+
# Belt plugin for Amazon Connect (Connect Customer): a chat widget and
|
|
8
|
+
# call-center contact flow, extracted and generalized from Stowzilla's
|
|
9
|
+
# standalone connect-infrastructure. Scaffolds Terraform the host app owns
|
|
10
|
+
# plus a frontend widget loader. The gem never applies AWS itself.
|
|
11
|
+
module Connect
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
class ConfigurationError < Error; end
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
attr_writer :configuration
|
|
17
|
+
|
|
18
|
+
def configuration
|
|
19
|
+
@configuration ||= Configuration.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def configure
|
|
23
|
+
yield(configuration)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reset_configuration!
|
|
27
|
+
@configuration = Configuration.new
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Required after Belt::Connect::Error is defined (SnippetParser::ParseError
|
|
34
|
+
# inherits from it).
|
|
35
|
+
require_relative 'connect/snippet_parser'
|